pretext实现文字居中环绕图片实例页面

回到相关文章 »

效果:

代码:

HTML代码:
<canvas id="canvas"></canvas>
                
JS代码:
<script type="importmap">
    { "imports": { "@chenglou/pretext": "https://esm.sh/@chenglou/pretext@0.0.6" } }
  </script>

  <script type="module">
    import { prepareWithSegments, layoutNextLineRange, materializeLineRange } from '@chenglou/pretext'

    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    
    const FONT = '32px system-ui'
    const LINE_HEIGHT = 48
    const PADDING = 16
    const IMAGE_WIDTH = 240
    const IMAGE_HEIGHT = 320
    const IMAGE_MARGIN = 24

    let mouseX = 0
    let mouseY = 0
    let isDragging = false

    const text = `字节跳动...`

    const prepared = prepareWithSegments(text, FONT)
    const img = new Image()
    img.src = './source-min.png'
    img.onload = () => {
      updateDefaultPosition()
      render()
    }

    function updateDefaultPosition() {
      const width = canvas.clientWidth
      const contentHeight = calculateContentHeight(width * 2)
      mouseX = width / 2 * 2
      mouseY = contentHeight / 2
    }
    
    function calculateContentHeight(width) {
      let cursor = { segmentIndex: 0, graphemeIndex: 0 }
      let y = PADDING
      while (cursor) {
        const range = layoutNextLineRange(prepared, cursor, width - PADDING * 2)
        if (!range) break
        cursor = range.end
        y += LINE_HEIGHT
      }
      let height = Math.max(y + PADDING, IMAGE_HEIGHT + PADDING * 2)
      
      // 图片占据的尺寸
      height += 4 * IMAGE_WIDTH * IMAGE_HEIGHT / width
      
      return height
    }
    
    function resize() {
      const width = canvas.clientWidth
      canvas.width = width * 2
      const contentHeight = calculateContentHeight(canvas.width)
      canvas.height = contentHeight + PADDING
      if (!isDragging) {
        updateDefaultPosition()
      }
      render()
    }

    function getLineWidth(y) {
      const containerWidth = canvas.width
      const imageLeft = mouseX - IMAGE_WIDTH / 2
      const imageRight = mouseX + IMAGE_WIDTH / 2
      const lineTop = y
      const lineBottom = y + LINE_HEIGHT
      
      if (lineBottom < lineTop || lineTop > lineBottom) return containerWidth - PADDING * 2
      
      const lineCenter = (lineTop + lineBottom) / 2
      const imageCenter = mouseY
      const distance = Math.abs(lineCenter - imageCenter)
      
      if (distance > IMAGE_HEIGHT / 2 + LINE_HEIGHT) {
        return containerWidth - PADDING * 2
      }

      const overlap = IMAGE_HEIGHT / 2 + LINE_HEIGHT - distance
      const factor = overlap / LINE_HEIGHT
      const blockedWidth = (IMAGE_WIDTH + IMAGE_MARGIN * 2) * factor
      
      const availableWidth = containerWidth - PADDING * 2 - blockedWidth
      return Math.max(availableWidth, 100)
    }

    function render() {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      
      ctx.font = FONT
      ctx.fillStyle = '#333'

      let cursor = { segmentIndex: 0, graphemeIndex: 0 }
      let y = PADDING

      while (cursor) {
        const width = getLineWidth(y)
        const range = layoutNextLineRange(prepared, cursor, width)
        
        if (!range) break
        
        const line = materializeLineRange(prepared, range)
        
        let x = PADDING
        const imageLeft = mouseX - IMAGE_WIDTH / 2
        const imageRight = mouseX + IMAGE_WIDTH / 2
        const lineTop = y
        const lineBottom = y + LINE_HEIGHT
        
        const lineCenter = (lineTop + lineBottom) / 2
        const imageCenter = mouseY
        const distance = Math.abs(lineCenter - imageCenter)
        
        if (distance < IMAGE_HEIGHT / 2 + LINE_HEIGHT) {
          const overlap = IMAGE_HEIGHT / 2 + LINE_HEIGHT - distance
          const factor = overlap / LINE_HEIGHT
          
          if (factor > 0.1) {
            if (line.text.length > 0) {
              const availableSpaceLeft = imageLeft - PADDING - IMAGE_MARGIN
              const availableSpaceRight = canvas.width - imageRight - PADDING - IMAGE_MARGIN
              
              if (availableSpaceLeft > 50) {
                const chars = Math.floor((availableSpaceLeft / width) * line.text.length)
                const leftPart = line.text.slice(0, chars)
                const rightPart = line.text.slice(chars)
                
                ctx.fillText(leftPart, x, y + 14)
                
                if (rightPart.length > 0 && availableSpaceRight > 50) {
                  ctx.fillText(rightPart, imageRight + IMAGE_MARGIN, y + 14)
                }
              } else {
                ctx.fillText(line.text, imageRight + IMAGE_MARGIN, y + 14)
              }
            }
          } else {
            ctx.fillText(line.text, x, y + 14)
          }
        } else {
          ctx.fillText(line.text, x, y + 14)
        }
        
        cursor = range.end
        y += LINE_HEIGHT
        
        if (y > canvas.height) break
      }

      ctx.save()
      ctx.drawImage(img, mouseX - IMAGE_WIDTH / 2, mouseY - IMAGE_HEIGHT / 2, IMAGE_WIDTH, IMAGE_HEIGHT)
      ctx.restore()
    }

    function handleMove(e) {
      if (!isDragging) return
      const rect = canvas.getBoundingClientRect()
      const scaleX = canvas.width / rect.width
      const scaleY = canvas.height / rect.height
      mouseX = (e.clientX - rect.left) * scaleX
      mouseY = (e.clientY - rect.top) * scaleY
      render()
    }

    function handleDown(e) {
      const rect = canvas.getBoundingClientRect()
      const scaleX = canvas.width / rect.width
      const scaleY = canvas.height / rect.height
      const clickX = (e.clientX - rect.left) * scaleX
      const clickY = (e.clientY - rect.top) * scaleY
      
      if (clickX >= mouseX - IMAGE_WIDTH / 2 && clickX <= mouseX + IMAGE_WIDTH / 2 &&
          clickY >= mouseY - IMAGE_HEIGHT / 2 && clickY <= mouseY + IMAGE_HEIGHT / 2) {
        isDragging = true
      }
    }

    function handleUp() {
      isDragging = false
    }

    resize()
    window.addEventListener('resize', resize)
    canvas.addEventListener('mousemove', handleMove)
    canvas.addEventListener('mousedown', handleDown)
    window.addEventListener('mouseup', handleUp)
    canvas.addEventListener('touchstart', (e) => {
      const touch = e.touches[0]
      const rect = canvas.getBoundingClientRect()
      const scaleX = canvas.width / rect.width
      const scaleY = canvas.height / rect.height
      const clickX = (touch.clientX - rect.left) * scaleX
      const clickY = (touch.clientY - rect.top) * scaleY
      
      if (clickX >= mouseX - IMAGE_WIDTH / 2 && clickX <= mouseX + IMAGE_WIDTH / 2 &&
          clickY >= mouseY - IMAGE_HEIGHT / 2 && clickY <= mouseY + IMAGE_HEIGHT / 2) {
        isDragging = true
        e.preventDefault()
      }
    })
    canvas.addEventListener('touchmove', (e) => {
      if (!isDragging) return
      e.preventDefault()
      const touch = e.touches[0]
      const rect = canvas.getBoundingClientRect()
      const scaleX = canvas.width / rect.width
      const scaleY = canvas.height / rect.height
      mouseX = (touch.clientX - rect.left) * scaleX
      mouseY = (touch.clientY - rect.top) * scaleY
      render()
    })
    window.addEventListener('touchend', handleUp)
  </script>