设置画布缩放原点

17

我想在画布上创建缩放效果,已经实现了,但是存在一个小问题。缩放(比例)的原点在画布的左上角。如何指定缩放/比例的原点?

我想我需要使用translate,但我不知道在哪里以及如何实现它。

我想要用作缩放原点的是鼠标位置,但为简单起见,画布中心也可以。

JSFiddle

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
canvas.width = 600;
canvas.height = 400;

var global = {
  zoom: {
    origin: {
      x: null,
      y: null,
    },
    scale: 1,
  },
};

function zoomed(number) {
  return Math.floor(number * global.zoom.scale);
}

function draw() {
  context.beginPath();
  context.rect(zoomed(50), zoomed(50), zoomed(100), zoomed(100));
  context.fillStyle = 'skyblue';
  context.fill();

  context.beginPath();
  context.arc(zoomed(350), zoomed(250), zoomed(50), 0, 2 * Math.PI, false);
  context.fillStyle = 'green';
  context.fill();
}

draw();

canvas.addEventListener("wheel", trackWheel);
canvas.addEventListener("wheel", zoom);

function zoom() {
  context.setTransform(1, 0, 0, 1, 0, 0);
  context.clearRect(0, 0, canvas.width, canvas.height);
  draw();
}

function trackWheel(e) {
  if (e.deltaY < 0) {
    if (global.zoom.scale < 5) {
      global.zoom.scale *= 1.1;
    }
  } else {
    if (global.zoom.scale > 0.1) {
      global.zoom.scale *= 0.9;
    }
  }
  global.zoom.scale = parseFloat(global.zoom.scale.toFixed(2));
}
body {
  background: gainsboro;
  margin: 0;
}
canvas {
  background: white;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>


更新1

看起来SO上还有一些与这个主题相关的问题,但没有一个我可以直接在我的代码中实现。

我试图检查Phrogz在Zoom Canvas to Mouse Cursor中提供的演示,但它太过复杂(至少对我来说是)。尝试实现他的解决方案:

ctx.translate(pt.x,pt.y);
ctx.scale(factor,factor);
ctx.translate(-pt.x,-pt.y);

JSFiddle

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
canvas.width = 600;
canvas.height = 400;

var global = {
  zoom: {
    origin: {
      x: null,
      y: null,
    },
    scale: 1,
  },
};

function draw() {
  context.beginPath();
  context.rect(50, 50, 100, 100);
  context.fillStyle = 'skyblue';
  context.fill();

  context.beginPath();
  context.arc(350, 250, 50, 0, 2 * Math.PI, false);
  context.fillStyle = 'green';
  context.fill();
}

draw();

canvas.addEventListener("wheel", trackWheel);
canvas.addEventListener("wheel", trackMouse);
canvas.addEventListener("wheel", zoom);

function zoom() {
  context.setTransform(1, 0, 0, 1, 0, 0);
  context.clearRect(0, 0, canvas.width, canvas.height);

  context.translate(global.zoom.origin.x, global.zoom.origin.y);
  context.scale(global.zoom.scale, global.zoom.scale);
  context.translate(-global.zoom.origin.x, -global.zoom.origin.y);

  draw();
}

function trackWheel(e) {
  if (e.deltaY > 0) {
    if (global.zoom.scale > 0.1) {
      global.zoom.scale *= 0.9;
    }
  } else {
    if (global.zoom.scale < 5) {
      global.zoom.scale *= 1.1;
    }
  }
  global.zoom.scale = parseFloat(global.zoom.scale.toFixed(2));
}

function trackMouse(e) {
  global.zoom.origin.x = e.clientX;
  global.zoom.origin.y = e.clientY;
}
body {
  background: gainsboro;
  margin: 0;
}
canvas {
  background: white;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>

但是它并没有真正帮助。似乎使用鼠标位置作为缩放起点,但当我缩放时会出现“跳跃”。


更新2

我已经成功地将Blindman67示例中的缩放效果进行了隔离和简化,以更好地理解它的工作方式。我必须承认,我仍然不完全理解它 :) 我将在这里分享它。未来的访客可能会受益。

JSFiddle

var canvas    = document.getElementById("canvas");
var context   = canvas.getContext("2d");
canvas.width  = 600;
canvas.height = 400;

var zoom = {
  scale : 1,
  screen : {
    x : 0,
    y : 0,
  },
  world : {
    x : 0,
    y : 0,
  },
};

var mouse = {
  screen : {
    x : 0,
    y : 0,
  },
  world : {
    x : 0,
    y : 0,
  },
};

var scale = {
  length : function(number) {
    return Math.floor(number * zoom.scale);
  },
  x : function(number) {
    return Math.floor((number - zoom.world.x) * zoom.scale + zoom.screen.x);
  },
  y : function(number) {
    return Math.floor((number - zoom.world.y) * zoom.scale + zoom.screen.y);
  },
  x_INV : function(number) {
    return Math.floor((number - zoom.screen.x) * (1 / zoom.scale) + zoom.world.x);
  },
  y_INV : function(number) {
    return Math.floor((number - zoom.screen.y) * (1 / zoom.scale) + zoom.world.y);
  },
};

function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);

  context.beginPath();
  context.rect(scale.x(50), scale.y(50), scale.length(100), scale.length(100));
  context.fillStyle = 'skyblue';
  context.fill();

  context.beginPath();
  context.arc(scale.x(350), scale.y(250), scale.length(50), 0, 2 * Math.PI, false);
  context.fillStyle = 'green';
  context.fill();
}

canvas.addEventListener("wheel", zoomUsingCustomScale);

function zoomUsingCustomScale(e) {
  trackMouse(e);
  trackWheel(e);
  scaleShapes();
}

function trackMouse(e) {
  mouse.screen.x = e.clientX;
  mouse.screen.y = e.clientY;
  mouse.world.x  = scale.x_INV(mouse.screen.x);
  mouse.world.y  = scale.y_INV(mouse.screen.y);
}

function trackWheel(e) {
  if (e.deltaY < 0) {
    zoom.scale = Math.min(5, zoom.scale * 1.1);
  } else {
    zoom.scale = Math.max(0.1, zoom.scale * (1/1.1));
  }
}

function scaleShapes() {
  zoom.screen.x = mouse.screen.x;
  zoom.screen.y = mouse.screen.y;
  zoom.world.x = mouse.world.x;
  zoom.world.y = mouse.world.y;
  mouse.world.x = scale.x_INV(mouse.screen.x);
  mouse.world.y = scale.y_INV(mouse.screen.y);
  draw();
}

draw();
body {
  background: gainsboro;
  margin: 0;
}

canvas {
  background: white;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>

由于这是一个简化版本,建议您先查看Blindman67的示例。此外,即使我已经接受了Blindman67的答案,您仍然可以发布答案。我发现这个主题很有趣。所以我想更多地了解它。


请查看这个之前的答案 这里 - Monica Olejniczak
@MonicaOlejniczak 是的,我已经看过了。当你评论时,我正在更新问题。他的演示太复杂了,我无法完美地实现他的解决方案。 - akinuri
当缩放时,如果将画布缓存到第二个内存画布中并使用该第二个画布作为主画布的绘制源,则可以获得更好的性能。当不进行缩放时,直接使用主画布进行绘制。这里有一个之前的问答,展示了如何缩放到鼠标位置。 - markE
@markE 这个代码在动态画布上还能用吗?我最终会缩放一个画布动画。现在只是在尝试。更新后的代码似乎只要缩放原点固定就可以很好地缩放。在缩放时缩放原点略微改变(如鼠标指针位置)会导致“跳跃”。Phrogz 的演示似乎没有这个问题。 - akinuri
缩放固定画布内容(使用第二个内存画布),没问题。缩放不断变化的画布内容 - 谁知道呢,你需要测试你的应用程序来看它的行为如何。 - markE
1个回答

29
如果只是缩放和平移,解决方案就很简单。您需要跟踪两个起点。一个是鼠标在世界坐标系中的位置(框和圆的位置),另一个是鼠标在屏幕坐标系(画布像素)中的位置。您需要习惯于从一个坐标系转换为另一个坐标系。可以通过反函数来完成。从世界坐标到屏幕坐标可以通过反函数翻转,该反函数将从屏幕坐标转换为世界坐标。
一些简单函数的倒数示例
2 * 10 = 20 的倒数是 20 / 10 = 2 2 + 3 = 5 的倒数是 5 - 3 = 2 (3 - 1) * 5 = 10 的倒数是 10 * (1/5) + 1 = 3
乘法变成* 1 over。例如 x*5 变成 x * 1/5(或者只是 x/5) 加成减,减成加,第一个变成最后一个,最后一个变成第一个 (3-first) * last = result 的倒数是 result / last + first = 3 因此,您缩放一个坐标(框的世界坐标位置)并获取框的屏幕像素位置。如果要获得屏幕像素的世界坐标,则应用反函数。
以下是您需要的代码,其中包含一些注释。我添加了mousemove、按钮等内容,因为您需要鼠标位置,如果不能平移,则没有缩放的意义,需要停止鼠标按钮锁定和停止滚轮滚动等等... 要平移,只需在 UI 中移动世界原点(在代码中)。另外,我有点懒,把 global.zoom.origin.x 的东西都去掉了,现在 scale 就是您知道的,wx、wy、sx、sy 是起点,请阅读代码以了解其含义。

var canvas    = document.getElementById("canvas");
var context   = canvas.getContext("2d");
canvas.width  = 600;
canvas.height = 400;

// lazy programmers globals
var scale = 1;
var wx    = 0; // world zoom origin
var wy    = 0;
var sx    = 0; // mouse screen pos
var sy    = 0;

var mouse = {};
mouse.x   = 0; // pixel pos of mouse
mouse.y   = 0;
mouse.rx  = 0; // mouse real (world) pos
mouse.ry  = 0;
mouse.button = 0;

function zoomed(number) { // just scale
  return Math.floor(number * scale);
}
// converts from world coord to screen pixel coord
function zoomedX(number) { // scale & origin X
  return Math.floor((number - wx) * scale + sx);
}

function zoomedY(number) { // scale & origin Y
  return Math.floor((number - wy) * scale + sy);
}

// Inverse does the reverse of a calculation. Like (3 - 1) * 5 = 10   the inverse is 10 * (1/5) + 1 = 3
// multiply become 1 over ie *5 becomes * 1/5  (or just /5)
// Adds become subtracts and subtract become add.
// and what is first become last and the other way round.

// inverse function converts from screen pixel coord to world coord
function zoomedX_INV(number) { // scale & origin INV
  return Math.floor((number - sx) * (1 / scale) + wx);
  // or return Math.floor((number - sx) / scale + wx);
}

function zoomedY_INV(number) { // scale & origin INV
  return Math.floor((number - sy) * (1 / scale) + wy);
  // or return Math.floor((number - sy) / scale + wy);
}

// draw everything in pixels coords
function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  
  context.beginPath();
  context.rect(zoomedX(50), zoomedY(50), zoomed(100), zoomed(100));
  context.fillStyle = 'skyblue';
  context.fill();

  context.beginPath();
  context.arc(zoomedX(350), zoomedY(250), zoomed(50), 0, 2 * Math.PI, false);
  context.fillStyle = 'green';
  context.fill();
}
// wheel event must not be passive to allow default action to be prevented
canvas.addEventListener("wheel", trackWheel, {passive:false}); 
canvas.addEventListener("mousemove", move)
canvas.addEventListener("mousedown", move)
canvas.addEventListener("mouseup", move)
canvas.addEventListener("mouseout", move) // to stop mouse button locking up 

function move(event) { // mouse move event
  if (event.type === "mousedown") {
    mouse.button = 1;
  }
  else if (event.type === "mouseup" || event.type === "mouseout") {
    mouse.button = 0;
  }

  mouse.bounds = canvas.getBoundingClientRect();
  mouse.x = event.clientX - mouse.bounds.left;
  mouse.y = event.clientY - mouse.bounds.top;
  var xx  = mouse.rx; // get last real world pos of mouse
  var yy  = mouse.ry;

  mouse.rx = zoomedX_INV(mouse.x); // get the mouse real world pos via inverse scale and translate
  mouse.ry = zoomedY_INV(mouse.y);
  if (mouse.button === 1) { // is mouse button down 
    wx -= mouse.rx - xx; // move the world origin by the distance 
    // moved in world coords
    wy -= mouse.ry - yy;
    // recaculate mouse world 
    mouse.rx = zoomedX_INV(mouse.x);
    mouse.ry = zoomedY_INV(mouse.y);
  }
  draw();
}

function trackWheel(e) {
  
  if (e.deltaY < 0) {
    scale = Math.min(5, scale * 1.1); // zoom in
  } else {
    scale = Math.max(0.1, scale * (1 / 1.1)); // zoom out is inverse of zoom in
  }
  wx = mouse.rx; // set world origin
  wy = mouse.ry;
  sx = mouse.x; // set screen origin
  sy = mouse.y;
  mouse.rx = zoomedX_INV(mouse.x); // recalc mouse world (real) pos
  mouse.ry = zoomedY_INV(mouse.y);
  
  draw();
  e.preventDefault(); // stop the page scrolling
}
draw();
body {
  background: gainsboro;
  margin: 0;
}
canvas {
  background: white;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
<canvas id="canvas"></canvas>


我喜欢这个。谢谢你。我已经完成了平移效果,但你将它们结合起来,这很棒。已点赞,但首先让我处理一下代码 :) - akinuri
我已经隔离了平移效果,并且我认为我开始理解它是如何工作的。然而,我有一个问题。是否可以像Phrogz的demo中那样,使用translatescale来完成这个操作,而不是缩放绘制对象的x、y、width、height - akinuri
@akinuri 首先掌握这个可能会很有帮助。您可以使用矩阵来完成,过程基本相同。矩阵只是提供了一些数学计算的捷径。这个例子https://dev59.com/S5nga4cB1Zd3GeqPYGc1#38676266展示了一个更复杂的版本。(图片加载需要时间,有时会比较慢)还有一种纯矩阵数学方法,但我还没有在这里发布过类似的内容。 - Blindman67
你好Blindman67。如果我抢占了这个评论区,希望这没有问题。 我已经花费数月时间开发一款游戏,但是在缩放功能上遇到了很大的困难。更具体地说,我的问题是:如何跟踪一个已经被缩放的对象的位置(即在画布上绘制对象原点x/y,缩放周围->我无法再检查mousemove事件x/y与对象的位置是否正确,因为偏移量不正确)。请查看这个完整的Codepen,这将对我非常有帮助。我花了很多时间,但还是没解决... http://codepen.io/AncientSion/pen/zZNqBr - user431806
@Blindman67,哪个选项最快?是translatescale,矩阵还是单独缩放画布上的项目? - Hristo Enev
@HristoEnev 使用变换矩阵比在绘制时计算坐标要快得多。矩阵默认应用,因此将矩阵设置为其他内容不会影响性能(除了放大会绘制更多像素,因此速度较慢)。这是我在该主题上添加的最新答案https://dev59.com/KqHia4cB1Zd3GeqPY8vx#44015705 - Blindman67

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接