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 = 0
  const IMAGE_WIDTH = 240
  const IMAGE_HEIGHT = 320
  const IMAGE_MARGIN = 12

  let mouseX = 0
  let mouseY = 0
  let isDragging = false
  let imageData = null
  let pixelMask = []

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

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

  function processImage() {
    const tempCanvas = document.createElement('canvas')
    tempCanvas.width = IMAGE_WIDTH
    tempCanvas.height = IMAGE_HEIGHT
    const tempCtx = tempCanvas.getContext('2d')
    tempCtx.drawImage(img, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT)
    
    imageData = tempCtx.getImageData(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT)
    const data = imageData.data
    
    pixelMask = []
    for (let y = 0; y < IMAGE_HEIGHT; y++) {
      const row = []
      for (let x = 0; x < IMAGE_WIDTH; x++) {
        const index = (y * IMAGE_WIDTH + x) * 4
        const alpha = data[index + 3]
        row.push(alpha > 128)
      }
      pixelMask.push(row)
    }
  }

  function getMaskedWidth(lineY) {
    const containerWidth = canvas.width
    const imageLeft = mouseX - IMAGE_WIDTH / 2
    const imageRight = mouseX + IMAGE_WIDTH / 2
    
    const lineCenter = lineY + LINE_HEIGHT / 2
    const imageTop = mouseY - IMAGE_HEIGHT / 2
    const imageBottom = mouseY + IMAGE_HEIGHT / 2
    
    if (lineCenter < imageTop - LINE_HEIGHT || lineCenter > imageBottom + LINE_HEIGHT) {
      return containerWidth - PADDING * 2
    }
    
    const maskY = Math.floor(lineCenter - imageTop)
    if (maskY < 0 || maskY >= IMAGE_HEIGHT) {
      return containerWidth - PADDING * 2
    }
    
    const row = pixelMask[maskY]
    if (!row) {
      return containerWidth - PADDING * 2
    }
    
    let leftBlocked = IMAGE_WIDTH
    let rightBlocked = -1
    
    for (let x = 0; x < IMAGE_WIDTH; x++) {
      if (row[x]) {
        leftBlocked = Math.min(leftBlocked, x)
        rightBlocked = Math.max(rightBlocked, x)
      }
    }
    
    if (leftBlocked > rightBlocked) {
      return containerWidth - PADDING * 2
    }
    
    const actualLeft = imageLeft + leftBlocked - IMAGE_MARGIN
    const actualRight = imageRight - (IMAGE_WIDTH - 1 - rightBlocked) + IMAGE_MARGIN
    const blockedWidth = actualRight - actualLeft
    
    return Math.max(containerWidth - PADDING * 2 - blockedWidth, 50)
  }

  function getLineSegments(lineY) {
    const containerWidth = canvas.width
    const imageLeft = mouseX - IMAGE_WIDTH / 2
    const imageRight = mouseX + IMAGE_WIDTH / 2
    
    const lineCenter = lineY + LINE_HEIGHT / 2
    const imageTop = mouseY - IMAGE_HEIGHT / 2
    const imageBottom = mouseY + IMAGE_HEIGHT / 2
    
    if (lineCenter < imageTop - LINE_HEIGHT || lineCenter > imageBottom + LINE_HEIGHT) {
      return [{ start: PADDING, end: containerWidth - PADDING }]
    }
    
    const maskY = Math.floor(lineCenter - imageTop)
    if (maskY < 0 || maskY >= IMAGE_HEIGHT) {
      return [{ start: PADDING, end: containerWidth - PADDING }]
    }
    
    const row = pixelMask[maskY]
    if (!row) {
      return [{ start: PADDING, end: containerWidth - PADDING }]
    }
    
    const segments = []
    let inBlock = false
    let blockStart = 0
    
    for (let x = 0; x <= IMAGE_WIDTH; x++) {
      const hasPixel = x < IMAGE_WIDTH && row[x]
      
      if (hasPixel && !inBlock) {
        inBlock = true
        blockStart = x
      } else if (!hasPixel && inBlock) {
        inBlock = false
        segments.push({ 
          type: 'block', 
          start: imageLeft + blockStart, 
          end: imageLeft + x 
        })
      }
    }
    
    if (segments.length === 0) {
      return [{ start: PADDING, end: containerWidth - PADDING }]
    }
    
    const result = []
    let lastEnd = PADDING
    
    segments.sort((a, b) => a.start - b.start)
    
    for (const block of segments) {
      const blockStartWithMargin = block.start - IMAGE_MARGIN
      const blockEndWithMargin = block.end + IMAGE_MARGIN
      
      if (blockStartWithMargin > lastEnd) {
        result.push({ 
          start: lastEnd, 
          end: Math.min(blockStartWithMargin, containerWidth - PADDING) 
        })
      }
      lastEnd = blockEndWithMargin
    }
    
    if (lastEnd < containerWidth - PADDING) {
      result.push({ start: lastEnd, end: containerWidth - PADDING })
    }
    
    return result.length > 0 ? result : [{ start: PADDING, end: containerWidth - PADDING }]
  }

  const article = document.querySelector('article')
  
  function updateDefaultPosition() {
    const canvasWidth = canvas.clientWidth
    const contentHeight = calculateContentHeight(canvasWidth * 2)
    mouseX = canvasWidth / 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
    }
    return Math.max(y + PADDING, IMAGE_HEIGHT + PADDING * 2) + 2 * IMAGE_WIDTH * IMAGE_HEIGHT / width
  }
  
  function resize() {
    const canvasWidth = canvas.clientWidth
    canvas.width = canvasWidth * 2
    const contentHeight = calculateContentHeight(canvas.width)
    canvas.height = contentHeight
    if (!isDragging) {
      updateDefaultPosition()
    }
    render()
  }

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

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

    while (cursor) {
      const segments = getLineSegments(y)
      
      for (const segment of segments) {
        if (!cursor) break
        
        const width = segment.end - segment.start
        if (width < 50) continue
        
        const range = layoutNextLineRange(prepared, cursor, width)
        if (!range) break
        
        const line = materializeLineRange(prepared, range)
        if (line.text.length > 0) {
          ctx.fillText(line.text, segment.start, y)
        }
        
        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>