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

响应式Canvas中SVG图像意外裁剪的技术问题求助

问题解决:响应式Canvas中SVG图像裁剪问题

问题概述

将固定尺寸(45×45)的SVG绘制到响应式HTML5 Canvas时,通过设置canvas.width/height避免变形,使用drawImage多参数形式保持比例,但画布宽高不一致时SVG出现意外裁剪。

问题根源

  1. SVG缺失viewBox属性:原SVG仅设置width/height,无viewBox导致缩放时内部元素无法正确适配目标尺寸,易出现偏移或裁剪。
  2. Image异步加载未处理:代码直接启动绘制循环,但SVG转成的Image是异步加载的,未等待加载完成就绘制会引发尺寸计算错误。
  3. drawImage参数错误:窄屏模式用canvas.height作为绘制尺寸,若画布宽度小于高度,图像会超出画布宽度被裁剪;宽屏模式同理,若画布高度小于宽度,图像会超出高度被裁剪。

解决方案

1. 修复SVG,添加viewBox属性

修改SVG标签,添加viewBox="0 0 45 45",确保SVG缩放时内部元素能正确映射到目标尺寸:

<svg id="pieceSVG" xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45" viewBox="0 0 45 45">
  <!-- 原SVG内容不变 -->
</svg>

2. 等待Image加载完成后再绘制

在Image的onload事件中启动绘制循环,避免加载未完成时的错误绘制:

const img = new Image();
img.src = imageUrl;
// 移除手动设置的width/height,让Image自动读取SVG尺寸
// img.height = 100;
// img.width = 100;

img.onload = () => {
  eventLoop(); // 加载完成后启动绘制循环
};

3. 修正drawImage的绘制尺寸计算

取画布宽高中的较小值作为绘制尺寸,保证图像完整显示在画布内,同时保持1:1比例:

const drawPiece = () => {
  if (!ctx) return;
  // 清空画布,避免重复绘制叠加
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // 计算最大可绘制尺寸(不超出画布)
  const maxSize = Math.min(canvas.width, canvas.height);
  // 居中绘制图像(可选,若需要居中)
  const x = (canvas.width - maxSize) / 2;
  const y = (canvas.height - maxSize) / 2;
  // 保持SVG原比例绘制
  ctx.drawImage(img, 0, 0, img.width, img.height, x, y, maxSize, maxSize);
};

4. 项目中drawPieceByCoord函数优化

确保SVG已添加viewBox,且Image加载完成后再调用绘制,同时替换硬编码尺寸为图像自身尺寸:

export const drawPieceByCoord = (
  coord: Coord,
  squareWidth: number,
  piece: PieceType | PieceOffBoard,
  ctx: CanvasRenderingContext2D
) => {
  if (!ctx || !piece.image.complete) return; // 确保图像加载完成

  const img = piece.image;
  let { x, y } = coord;
  x = x - squareWidth / 2;
  y = y - squareWidth / 2;

  // 使用图像自身的宽高(而非硬编码45),适配可能的SVG尺寸变化
  ctx.drawImage(img, 0, 0, img.width, img.height, x, y, squareWidth, squareWidth);
};

完整修复后的示例代码

HTML部分

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
</head>
<body>
  <canvas id="dummyCanvas" style="background-color: blue"></canvas>
  <svg id="pieceSVG" xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45" viewBox="0 0 45 45">
    <g style="fill:#ffffff;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
      <path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"/>
      <path d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 11,36 11,36 C 9.5,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z"/>
      <path d="M 11.5,30 C 15,29 30,29 33.5,30" style="fill:none"/>
      <path d="M 12,33.5 C 18,32.5 27,32.5 33,33.5" style="fill:none"/>
      <circle cx="6" cy="12" r="2" />
      <circle cx="14" cy="9" r="2" />
      <circle cx="22.5" cy="8" r="2" />
      <circle cx="31" cy="9" r="2" />
      <circle cx="39" cy="12" r="2" />
    </g>
  </svg>
</body>
</html>

JavaScript部分

const pieceSVG = document.getElementById("pieceSVG");
const svgString = new XMLSerializer().serializeToString(pieceSVG);
const canvas = document.getElementById("dummyCanvas");
const ctx = canvas.getContext("2d");

const imageBlob = new Blob([svgString], { type: "image/svg+xml" });
const imageUrl = URL.createObjectURL(imageBlob);

const img = new Image();
img.src = imageUrl;

const setCanvasWidth = () => {
  return window.innerWidth < 720 ? window.innerWidth * 0.6 : window.innerWidth / 10;
};

const setCanvasHeight = () => {
  return window.innerWidth > 720 ? window.innerHeight * 0.6 : window.innerHeight / 10;
};

const setSize = () => {
  canvas.width = setCanvasWidth();
  canvas.height = setCanvasHeight();
};

const drawPiece = () => {
  if (!ctx) return;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const maxSize = Math.min(canvas.width, canvas.height);
  const x = (canvas.width - maxSize) / 2;
  const y = (canvas.height - maxSize) / 2;
  ctx.drawImage(img, 0, 0, img.width, img.height, x, y, maxSize, maxSize);
};

setSize();
window.addEventListener("resize", setSize);

const eventLoop = () => {
  requestAnimationFrame(() => {
    drawPiece();
    eventLoop();
  });
};

// 等待图像加载完成后启动循环
img.onload = () => {
  eventLoop();
};

内容的提问来源于stack exchange,提问作者JoshD

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 07:35:01