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>