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

复刻Aseprite画布定点缩放功能遇像素偏移问题求助

复刻Aseprite画布定点缩放时的像素偏移问题

我正尝试复刻Aseprite的画布定点缩放功能,但当前代码在画布不同区域进行缩放操作时,会出现像素偏移的问题。已知该问题对应Stack Overflow上《Zoom in on a point (using scale and translate)》的重复问题,参考过相关解答但无法适配现有代码,请求技术协助。

效果说明

  • 问题效果:缩放时画布会偏移,鼠标指向的画布位置无法保持在原屏幕位置
  • 预期效果:Aseprite式缩放,鼠标指向的画布区域在缩放过程中始终固定在当前鼠标位置

原问题代码

<html>
  <body>
    <main>
      <section Id="canvas-panel">
        <section
          id="canvas"
          style="width: 256px; height: 256px; background-size: 12.5%"
        >
          <canvas width="256px" height="256px"></canvas>
        </section>
      </section>
    </main>
  </body>
</html>

<script>
  let canvasPanel = document.getElementById("canvas-panel");
  let canvas = document.getElementById("canvas");

  // Function to calculate the closest point on the border of the canvas to the click position.
  function calculateClosestBorderPoint(event) {
    const canvasRect = canvas.getBoundingClientRect();

    const clickX = event.clientX;
    const clickY = event.clientY;

    let closestX = canvasRect.left;
    let closestY = canvasRect.top;

    // Determine if the click is closer to the left or right side of the canvas.
    if (clickX < canvasRect.left) {
      closestX = canvasRect.left;
    } else if (clickX > canvasRect.right) {
      closestX = canvasRect.right;
    } else {
      closestX = clickX;
    }

    // Determine if the click is closer to the top or bottom of the canvas.
    if (clickY < canvasRect.top) {
      closestY = canvasRect.top;
    } else if (clickY > canvasRect.bottom) {
      closestY = canvasRect.bottom;
    } else {
      closestY = clickY;
    }

    return { x: closestX, y: closestY };
  }

  // Initialize scale.
  let scale = 1;

  // Define zoom limits
  const minScale = 0.5; // Minimum zoom limit.
  const maxScale = 4.0; // Maximum zoom limit.

  // Function to update the canvas transformation.
  function updateCanvasTransform() {
    canvas.style.transform = `scale(${scale})`;
  }

  canvasPanel.addEventListener("wheel", function (e) {
    e.preventDefault();

    const zoomDirection = e.deltaY < 0 ? 1 : -1;

    if (scale == minScale && zoomDirection == -1) return;
    if (scale == maxScale && zoomDirection == 1) return;

    const zoomFactor = 1.1;

    const closestPoint = calculateClosestBorderPoint(event);

    scale *= zoomFactor ** zoomDirection; // Apply zoom factor based on direction

    // Clamp scale to limits
    scale = Math.min(scale, maxScale);
    scale = Math.max(scale, minScale);

    // Update canvas position to keep the closest point centered relative to the viewport
    const canvasRect = canvas.getBoundingClientRect();
    const viewportCenterX = canvasRect.width / 2;
    const viewportCenterY = canvasRect.height / 2;

    // Convert closest point to relative position within the viewport (0-1 scale)
    let relativeX =
      ((closestPoint.x - canvasRect.left) / canvasRect.width) * 100;
    let relativeY =
      ((closestPoint.y - canvasRect.top) / canvasRect.height) * 100;

    canvas.style.transformOrigin = `${relativeX}% ${relativeY}%`;

    // Update canvas transformation using only scale
    updateCanvasTransform();
  });
</script>

<style>
  #canvas {
    position: fixed;
    overflow: hidden;
    padding: 0;
    margin: 0;
    transform: scale(1) translate(0px, 0px);
    background-color: #1d1d1d;
  }

  #canvas-panel {
    position: relative;
    width: 100%;
    height: 100%;
    background-color: #303030;
  }

  html,
  body {
    background-color: #161616;
    margin: 0;
    width: 100%;
    height: 100%;
  }
</style>

解决方案

核心思路是使用translate + scale的组合变换,严格按照定点缩放的数学公式计算偏移量,确保鼠标指向的画布位置在缩放前后保持在屏幕的同一位置。以下是修改后的完整代码:

<html>
  <body>
    <main>
      <section id="canvas-panel">
        <section
          id="canvas"
          style="width: 256px; height: 256px; background-size: 12.5%"
        >
          <canvas width="256px" height="256px"></canvas>
        </section>
      </section>
    </main>
  </body>
</html>

<script>
  const canvasPanel = document.getElementById("canvas-panel");
  const canvas = document.getElementById("canvas");
  
  // 初始化缩放和偏移量
  let scale = 1;
  let translateX = 0;
  let translateY = 0;
  
  // 缩放限制
  const minScale = 0.5;
  const maxScale = 4.0;
  const zoomFactor = 1.1;

  // 更新画布变换
  function updateCanvasTransform() {
    canvas.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
  }

  canvasPanel.addEventListener("wheel", function(e) {
    e.preventDefault();
    
    const zoomDirection = e.deltaY < 0 ? 1 : -1;
    const newScale = Math.min(Math.max(scale * (zoomFactor ** zoomDirection), minScale), maxScale);
    if (newScale === scale) return;
    
    // 获取鼠标相对于画布容器的坐标
    const panelRect = canvasPanel.getBoundingClientRect();
    const mouseX = e.clientX - panelRect.left;
    const mouseY = e.clientY - panelRect.top;
    
    // 获取画布原始尺寸(未缩放)
    const canvasWidth = canvas.offsetWidth;
    const canvasHeight = canvas.offsetHeight;
    
    // 计算缩放前后的偏移量变化,确保鼠标点位置不变
    const mouseCanvasX = (mouseX - translateX) / scale;
    const mouseCanvasY = (mouseY - translateY) / scale;
    
    // 更新缩放和偏移
    scale = newScale;
    translateX = mouseX - mouseCanvasX * scale;
    translateY = mouseY - mouseCanvasY * scale;
    
    updateCanvasTransform();
  });
</script>

<style>
  #canvas {
    position: absolute;
    overflow: hidden;
    padding: 0;
    margin: 0;
    transform-origin: 0 0; /* 固定变换原点为左上角,简化计算 */
    background-color: #1d1d1d;
  }

  #canvas-panel {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: #303030;
  }

  html,
  body {
    background-color: #161616;
    margin: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
</style>

关键修改点

  1. 固定变换原点:将transform-origin设为左上角(0 0),避免动态修改原点带来的计算误差
  2. 记录偏移量:新增translateX/Y变量跟踪画布的位置偏移
  3. 严格数学计算:根据鼠标位置、当前缩放比例,计算缩放后的偏移量,确保鼠标指向的画布坐标在缩放前后对应同一屏幕位置
  4. 简化容器布局:将画布的position改为absolute,容器添加overflow: hidden,避免固定定位的布局冲突

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 00:28:13