如何正确缩放HTML5 Canvas避免模糊并保持元素居中?
解决方案
核心问题在于:你通过ctx.scale(devicePixelRatio, devicePixelRatio)将Canvas的绘制坐标系映射到了父容器的显示尺寸(而非Canvas的实际像素尺寸),但计算中心坐标时仍在使用Canvas的实际宽高(canvas.width/canvas.height),导致坐标偏移。以下是分步修复方案:
1. 移除JSX中Canvas的宽高属性
JSX中设置的width/height会被缩放函数覆盖,且初始值可能不准确,直接移除:
<canvas ref={canvasRef} className={style.canvas} />
2. 保存父容器显示尺寸到状态
将父容器的显示尺寸存入组件状态,避免重复DOM查询,同时解决元素不可见时的尺寸获取问题:
import { useState, useEffect, useRef } from 'react'; // 组件内定义状态 const [parentDisplaySize, setParentDisplaySize] = useState({ width: 0, height: 0 });
3. 修复缩放函数并同步尺寸状态
修改scaleCanvas函数,在缩放时同步父容器的显示尺寸到状态:
const scaleCanvas = ( canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D ): void => { const canvasParent = canvasParentRef.current; if (!canvasParent) return; const { devicePixelRatio } = window; const { width, height } = canvasParent.getBoundingClientRect(); // 同步显示尺寸到状态 setParentDisplaySize({ width, height }); // 设置Canvas实际像素尺寸(解决模糊问题) canvas.width = width * devicePixelRatio; canvas.height = height * devicePixelRatio; // 缩放绘制坐标系,使1单位绘制对应1px显示尺寸 ctx.scale(devicePixelRatio, devicePixelRatio); // 设置Canvas显示尺寸 canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; };
4. 修复中心矩形坐标计算
使用父容器的显示尺寸(而非Canvas实际像素尺寸)计算坐标,同时处理元素不可见时的 fallback:
const centerRectCoords = () => { const { width: rectW, height: rectH } = rect; const { width: canvasW, height: canvasH } = parentDisplaySize; // 处理元素不可见时的尺寸 fallback(比如用窗口尺寸) const targetW = canvasW || window.innerWidth; const targetH = canvasH || window.innerHeight; const startX = targetW / 2 - rectW / 2; const startY = targetH / 2 - rectH / 2; return { startX, startY, endX: startX + rectW, endY: startY + rectH, }; };
5. 监听尺寸变化与元素可见性
确保在窗口 resize、元素从不可见变为可见时,重新缩放并绘制:
useEffect(() => { const canvas = canvasRef.current; const canvasParent = canvasParentRef.current; if (!canvas || !canvasParent) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // 初始化绘制 const draw = () => { // 清空画布 ctx.clearRect(0, 0, parentDisplaySize.width, parentDisplaySize.height); // 获取中心矩形坐标并绘制 const { startX, startY, endX, endY } = centerRectCoords(); ctx.fillRect(startX, startY, endX - startX, endY - startY); // 绘制其他基于中心的矩形... }; scaleCanvas(canvas, ctx); draw(); // 监听窗口resize const handleResize = () => { scaleCanvas(canvas, ctx); draw(); }; window.addEventListener('resize', handleResize); // 监听元素可见性(解决标签页切换后尺寸异常) const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { scaleCanvas(canvas, ctx); draw(); } }); }); observer.observe(canvasParent); // 清理副作用 return () => { window.removeEventListener('resize', handleResize); observer.unobserve(canvasParent); }; }, [rect]); // 当rect尺寸变化时重新绘制
核心原理
- 通过
ctx.scale(devicePixelRatio, devicePixelRatio),将Canvas的绘制坐标系与显示尺寸对齐,此时你在绘制时使用的坐标单位就是用户看到的px,无需再手动处理DPR。 - 保存父容器的显示尺寸到状态,确保坐标计算始终基于当前的实际显示区域。
- 监听元素可见性,解决标签页切换后
getBoundingClientRect返回0的问题。
内容的提问来源于stack exchange,提问作者lukas23
相关产品推荐
相关产品推荐

