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

Canvas缩放拖拽时无限固定网格实现及Offset计算问题求助

问题求助:Canvas缩放拖拽时无限固定网格的offset计算

我参考博客重构代码,使用Canvas变换替代手动偏移,却无法实现缩放拖拽时的无限固定网格。已多次修改drawGrid()函数,目前正在研读《HTML Canvas Deep Dive》和《The HTML5 Canvas Handbook》。将drawGrid()放在ctx.stop()与ctx.restore()之间后,拖拽时网格可循环,但基于鼠标点缩放时的offset计算存在问题,求助正确的计算方法。

相关代码

JavaScript

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const wrapper = document.getElementsByClassName('wrapper')[0];

canvas.width = wrapper.clientWidth;
canvas.height = wrapper.clientHeight;

let scale = 1;
let previousScale = 1;
const maxScale = 3;
const minScale = 0.5;

const gridSize = 150;
const gridColor = '#888';

let offsetX = 0;
let offsetY = 0;

let isDragging = false;
let dragElement = null;
let dragOffsetX = 0;
let dragOffsetY = 0;

let dragStartPosition = { x: 0, y: 0 };
let currentTransformedCursor = { x: 0, y: 0 };


class CanvasElement {
  constructor(x, y, shape) {
    this.x = x;
    this.y = y;
    this.shape = shape;
  }

  isMouseInside(ctx, mouseX, mouseY) {
    ctx.save();
    ctx.beginPath();
    this.drawPath(ctx);
    const isInside = ctx.isPointInPath(mouseX, mouseY);
    ctx.restore();
    return isInside;
  }

  drawPath(ctx) {
    throw new Error('drawPath method must be implemented in the derived class');
  }
}

class Rectangle extends CanvasElement {
  constructor(x, y, width, height, color) {
    super(x, y, 'rect');
    this.width = width;
    this.height = height;
    this.color = color;
  }

  drawPath(ctx) {
    ctx.fillStyle = this.color;
    ctx.rect(this.x, this.y, this.width, this.height);
  }
}

class Circle extends CanvasElement {
  constructor(x, y, radius, color) {
    super(x, y, 'circle');
    this.radius = radius;
    this.color = color;
  }

  drawPath(ctx) {
    ctx.fillStyle = this.color;
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
  }
}

class Triangle extends CanvasElement {
  constructor(x, y, size, color) {
    super(x, y, 'triangle');
    this.size = size;
    this.color = color;
  }

  drawPath(ctx) {
    ctx.fillStyle = this.color;
    ctx.moveTo(this.x, this.y);
    ctx.lineTo(this.x + this.size, this.y);
    ctx.lineTo(this.x + this.size / 2, this.y + this.size);
    ctx.closePath();
  }
}

function getTransformedPoint(x, y) {
  const originalPoint = new DOMPoint(x, y);
  return ctx.getTransform().invertSelf().transformPoint(originalPoint);
}

const handleMouseDown = (event) => {
  isDragging = true;
  dragStartPosition = getTransformedPoint(event.offsetX, event.offsetY);

  const mouseX = event.clientX - canvas.offsetLeft;
  const mouseY = event.clientY - canvas.offsetTop;

  dragElement = elements.find(element => element.isMouseInside(ctx, mouseX, mouseY));

  if (dragElement) {
    dragOffsetX = mouseX - dragElement.x;
    dragOffsetY = mouseY - dragElement.y;
  }
}

const handleMouseMove = (event) => {
  currentTransformedCursor = getTransformedPoint(event.offsetX, event.offsetY);

  const mouseX = event.clientX - canvas.offsetLeft;
  const mouseY = event.clientY - canvas.offsetTop;
  const hoveredElement = elements.find(element => element.isMouseInside(ctx, mouseX, mouseY));


  if (isDragging) {
    if (dragElement) {
      dragElement.x = mouseX - dragOffsetX;
      dragElement.y = mouseY - dragOffsetY;
    }else{
      const deltaX = currentTransformedCursor.x - dragStartPosition.x;
      const deltaY = currentTransformedCursor.y - dragStartPosition.y;

      offsetX += deltaX;
      offsetY += deltaY;

      ctx.translate(deltaX, deltaY);
    }

    draw();
  }else{
    draw();
    if (hoveredElement) {
      ctx.strokeStyle = '#fff';
      ctx.lineWidth = 2;
      ctx.beginPath();
      hoveredElement.drawPath(ctx);
      ctx.stroke();
    }
  }
};

const handleMouseOut = (event) => {
  isDragging = false;
  dragElement = null;
  dragOffsetX = 0;
  dragOffsetY = 0;
}

const handleMouseUp = (event) => {
  isDragging = false;
  dragElement = null;
  dragOffsetX = 0;
  dragOffsetY = 0;
}

const handleWheel = (event) => {
  const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9;

  scale = previousScale * zoomFactor;

  if (scale >= minScale && scale <= maxScale) {
    previousScale = scale;

    const deltaX = currentTransformedCursor.x - dragStartPosition.x;
    const deltaY = currentTransformedCursor.y - dragStartPosition.y;

    offsetX += deltaX;
    offsetY += deltaY;

    ctx.translate(currentTransformedCursor.x, currentTransformedCursor.y);
    ctx.scale(zoomFactor, zoomFactor);
    ctx.translate(-currentTransformedCursor.x, -currentTransformedCursor.y);

    draw();
    event.preventDefault();
  }
}

function stopTransform(canvas) {
  var context = canvas.getContext('2d');
  context.setTransform(1, 0, 0, 1, 0, 0);
}

function drawGrid() {
  const updateGridSize = gridSize * scale;
  const gridOffsetX = offsetX % updateGridSize;
  const gridOffsetY = offsetY % updateGridSize;

  ctx.beginPath();
  for (let x = gridOffsetX; x < canvas.width; x += updateGridSize) {
    ctx.moveTo(x, 0);
    ctx.lineTo(x, canvas.height);
  }
  for (let y = gridOffsetY; y < canvas.height; y += updateGridSize) {
    ctx.moveTo(0, y);
    ctx.lineTo(canvas.width, y);
  }
  ctx.lineWidth = 1;
  ctx.strokeStyle = gridColor;
  ctx.stroke();
};

canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseout', handleMouseOut);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('wheel', handleWheel);

const rectangle = new Rectangle(50, 50, 100, 50, "#f00");
const circle = new Circle(200, 200, 50, "#0f0");
const circle2 = new Circle(360, 500, 90, "#af0");
const triangle = new Triangle(300, 100, 60, "#00f");

const elements = [rectangle, circle, circle2, triangle];

function draw() {
  ctx.save();

  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  drawGrid();

  ctx.restore();

  elements.forEach(element => {
    ctx.beginPath();
    element.drawPath(ctx);
    ctx.fill();
  });
}

draw();

CSS

* {
  padding: 0;
  margin: 0;
  overflow: hidden;
}
html, body {
  width: 100%;
  height: 100%;
  background-color: #a1a1a1;
}
canvas {
  display: block;
  box-sizing: border-box;
}
.wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  position: relative;
  z-index: 2;
}
.debug{
  position: absolute;
  width:auto;
  height:auto;
  right:0;
  background-color:none;
  text-align: right;
  margin: 20px;
  z-index:9;
}

HTML

<div class="wrapper">
  <div>
    <canvas id="canvas"></canvas>
  </div>
</div>

期望实现效果

拖拽时网格无缝循环延伸,缩放时以鼠标位置为中心,网格保持固定间距无限展开。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 22:42:00