You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确缩放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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 23:52:48