React中实现HTML Canvas输入时文字自适应圆形(带缩放)的问题
React中实现HTML Canvas输入时文字自适应圆形(带缩放)的问题
我懂你想要实现的效果——输入文字时让内容自动适配固定圆形的排版,试过Mike Bostock的教程和markE的旧方案但都卡壳了对吧?咱们先拆解你遇到的问题,再给出能跑通的修正方案。
你的两个尝试里的核心问题
第一个尝试的卡点
- State更新时机问题:你在
handleChange里先用了setText(newText),紧接着就用text.split,但React的State更新是异步的,这时候text还是旧值,得直接用newText来处理。 - 文字圆形排列逻辑缺失:你最后只是把文字按普通行绘制,没有结合圆形的弧度/弦长来计算每行的位置和宽度,这就没法实现“适配圆形”的效果。
第二个尝试的问题
- 未处理边界情况:
calcAllowableWords函数在所有文字都能塞进一行时没有返回值,导致lineData变成undefined,访问它的属性就会抛出“text undefined”的错误。 - 坐标计算偏差:绘制文字时的Y轴坐标计算有点问题,而且没有在每次输入时清空画布,导致旧文字会残留。
- 逻辑倒置:你提到的“半径适配文字而非文字适配半径”,是因为原来的方案默认是让圆跟着文字走,咱们需要改成固定圆半径,让文字去适配这个固定的圆。
修正后的完整实现方案
下面是调整后的代码,解决了上面的所有问题,实现了输入时文字自动适配固定圆形的效果:
import React, { useEffect, useRef, useState } from "react"; export const TwoPI = 2 * Math.PI; export function setupGridWidthHeightAndScale( width: number, height: number, canvas: HTMLCanvasElement ) { canvas.style.width = width + "px"; canvas.style.height = height + "px"; const scale = window.devicePixelRatio; canvas.width = width * scale; canvas.height = height * scale; const canvasCtx = canvas.getContext("2d")!; canvasCtx.scale(scale, scale); } type CanvasProps = { width: number; height: number; }; export function TextInCircle({ width, height }: CanvasProps) { const [typedText, setTypedText] = useState(""); const canvasRef = useRef<HTMLCanvasElement>(null); function getContext() { const canvas = canvasRef.current!; return canvas.getContext("2d")!; } // 初始化Canvas和绘制背景+固定圆形 useEffect(() => { const canvas = canvasRef.current!; setupGridWidthHeightAndScale(width, height, canvas); drawBaseCircle(); }, [width, height]); // 绘制基础的固定圆形 const drawBaseCircle = () => { const ctx = getContext(); ctx.clearRect(0, 0, width, height); // 背景 ctx.fillStyle = "black"; ctx.fillRect(0, 0, width, height); // 固定半径的圆形 const cx = width / 2; const cy = height / 2; const r = 100; ctx.beginPath(); ctx.arc(cx, cy, r, 0, TwoPI); ctx.closePath(); ctx.fillStyle = "white"; ctx.fill(); // 圆形边框(可选) ctx.strokeStyle = "#333"; ctx.lineWidth = 2; ctx.stroke(); }; // 计算圆形上每行的最大宽度(弦长)和Y轴位置 const getCircleLines = (r: number, lineHeight: number) => { const cx = width / 2; const cy = height / 2; const lines: { y: number; maxWidth: number }[] = []; // 从圆形顶部往下遍历,计算每行的弦长 for (let yOffset = -r + lineHeight; yOffset < r - lineHeight; yOffset += lineHeight) { // 计算当前行到圆心的垂直距离 const dy = Math.abs(yOffset); // 弦长公式:2*sqrt(r² - dy²) const chordLength = 2 * Math.sqrt(r * r - dy * dy); if (chordLength > 10) { // 过滤太窄的行 lines.push({ y: cy + yOffset, maxWidth: chordLength }); } } return lines; }; // 将文字拆分为适配每行宽度的行 const splitTextToLines = (text: string, lines: { y: number; maxWidth: number }[]) => { const ctx = getContext(); ctx.font = "12px Arial"; const words = text.trim().split(/\s+/); const resultLines: { y: number; text: string }[] = []; let currentLine = ""; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; const testWidth = ctx.measureText(testLine).width; // 找到当前可用的行 const targetLine = resultLines.length < lines.length ? lines[resultLines.length] : null; if (!targetLine) break; // 没有更多行可以放了 if (testWidth <= targetLine.maxWidth) { currentLine = testLine; } else { if (currentLine) { resultLines.push({ y: targetLine.y, text: currentLine }); } // 如果单个单词就超过行宽,直接截断(或者你可以做 hyphenation 处理) const truncatedWord = word.slice(0, Math.floor(targetLine.maxWidth / ctx.measureText("a").width)); resultLines.push({ y: targetLine.y, text: truncatedWord + "..." }); currentLine = ""; } } // 把最后一行加进去 if (currentLine && resultLines.length < lines.length) { resultLines.push({ y: lines[resultLines.length].y, text: currentLine }); } return resultLines; }; // 绘制适配圆形的文字 const drawTextInCircle = (text: string) => { const ctx = getContext(); const r = 100; const lineHeight = 18; // 行高(包含间距) const lines = getCircleLines(r, lineHeight); const textLines = splitTextToLines(text, lines); // 清空画布后重绘基础圆形 drawBaseCircle(); // 设置文字样式 ctx.font = "12px Arial"; ctx.fillStyle = "black"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; // 绘制每一行文字 textLines.forEach(line => { ctx.fillText(line.text, width / 2, line.y); }); }; const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const newText = e.target.value; setTypedText(newText); drawTextInCircle(newText); }; return ( <> <input type="text" onChange={handleChange} placeholder="输入文字试试..." style={{ marginBottom: "10px", padding: "8px" }} /> <canvas ref={canvasRef}></canvas> </> ); }
关键逻辑说明
- 固定圆形半径:我们先绘制一个固定半径的白色圆形,所有文字都要适配这个圆形的范围。
- 弦长计算:用几何公式计算圆形上每行的最大宽度(弦长),确保文字不会超出圆形边界。
- 文字分行适配:把输入的文字拆分成适配每行弦长的内容,单个单词过长时会自动截断。
- 实时重绘:每次输入时清空画布,重绘圆形和文字,避免旧内容残留。
- 高清Canvas处理:保留了你原来的
setupGridWidthHeightAndScale函数,解决Canvas模糊的问题。
这样调整后,你输入文字时,内容就会自动适配固定圆形的排版啦~
备注:内容来源于stack exchange,提问作者psygo
相关产品推荐
相关产品推荐

