React 调整<canvas>尺寸适配父div同时保留绘图功能的实现求助
问题根因定位
换用父div作为画布容器后交互失效,核心是两个之前被掩盖的逻辑问题被暴露:
- 坐标参考系错位:代码里直接取
nativeEvent.offsetX/offsetY作为绘图坐标,这两个值是相对实际触发事件的DOM元素计算的。之前画布铺满整个视窗时,没有父级偏移、没有边框占位,坐标刚好能对应上;现在画布嵌在带2px边框的div容器里,一旦鼠标靠近边缘,事件触发目标变成外层div,拿到的坐标是相对div左上角的,和canvas自身的坐标系差了边框的宽度、偏移量,直接导致绘制路径跑到画布外的无效区域,看起来就像功能失效。 - 初始化逻辑无响应能力:原来的画布尺寸更新逻辑只在组件挂载时执行一次,完全没法适配后续父容器的移动、缩放,尺寸和坐标系的偏差会越来越大。
适配实现方案
1. 统一坐标计算逻辑,彻底解决参考系错位
放弃依赖事件自带的offset坐标,主动计算鼠标位置相对canvas左上角的真实偏移,不管canvas放在页面哪个位置、有没有父级边框、是否动态移动,计算结果都能保证准确:
const getCanvasCoordinates = (e) => { const canvas = canvasRef.current const rect = canvas.getBoundingClientRect() return { x: e.clientX - rect.left, y: e.clientY - rect.top } }
在startDrawing和draw方法里,用这个方法的返回值替换原来的offsetX/offsetY取值。
2. 增加动态尺寸监听,适配可移动、可缩放的容器
抽离公共的尺寸更新逻辑,用ResizeObserver监听父容器的尺寸变化,同步更新画布的分辨率和显示尺寸,同时保留原来的2倍像素比设置,解决高清屏模糊问题:
useEffect(() => { const canvas = canvasRef.current const parentDiv = divRef.current const ctx = canvas.getContext("2d") contextRef.current = ctx const updateCanvasSize = () => { // 保留原有高清屏适配的2倍缩放逻辑 canvas.width = parentDiv.clientWidth * 2 canvas.height = parentDiv.clientHeight * 2 canvas.style.width = `${parentDiv.clientWidth}px` canvas.style.height = `${parentDiv.clientHeight}px` // 尺寸更新后重新设置上下文属性,避免属性失效 ctx.scale(2, 2) ctx.lineCap = "round" ctx.strokeStyle = "black" ctx.lineWidth = 5 } // 初始化时执行一次尺寸设置 updateCanvasSize() // 监听父容器尺寸变化,自动同步画布大小 const resizeObserver = new ResizeObserver(updateCanvasSize) resizeObserver.observe(parentDiv) // 组件卸载时销毁监听器,避免内存泄漏 return () => resizeObserver.disconnect() }, [])
3. 优化交互逻辑,降低边界异常概率
- 把原来用
useState存的绘图状态换成useRef存储,避免高频mousemove时反复触发React重渲染导致的卡顿 - 给canvas补上
onMouseLeave事件,鼠标移出画布范围时自动结束当前绘图,避免鼠标快速移动时事件丢帧导致的连线异常
const drawingRef = useRef(false) const startDrawing = (e) => { const { x, y } = getCanvasCoordinates(e) contextRef.current.beginPath() contextRef.current.moveTo(x, y) drawingRef.current = true } const finishDrawing = () => { contextRef.current.closePath() drawingRef.current = false } const draw = (e) => { if (!drawingRef.current) return const { x, y } = getCanvasCoordinates(e) contextRef.current.lineTo(x, y) contextRef.current.stroke() } // JSX部分给canvas补充onMouseLeave绑定 return ( <div id="canvasDiv" ref={divRef}> <canvas onMouseDown={startDrawing} onMouseUp={finishDrawing} onMouseMove={draw} onMouseLeave={finishDrawing} ref={canvasRef} /> </div> )
注意事项
- 尺寸更新时如果需要保留之前绘制的内容,可以在
updateCanvasSize方法里先把当前画布内容转成临时图片,更新完画布尺寸、重置上下文属性后,再把图片绘制回去,避免尺寸变更导致的画布清空 - CSS里父容器的边框占位会被
clientWidth自动扣除,不需要额外计算边框的宽度差,避免引入新的坐标偏差
内容的提问来源于stack exchange,提问作者bgardne7
相关产品推荐
相关产品推荐

