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>