【问题标题】:Set canvas zoom/scale origin设置画布缩放/缩放原点
【发布时间】:2016-12-13 02:37:22
【问题描述】:

我正在尝试在画布上创建缩放效果,并且我已经设法做到了,但是有一个小问题。缩放(缩放)原点位于画布的左上角。如何指定缩放/缩放原点?

我想我需要使用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);
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;


更新 1

似乎在 SO 上与此主题相关的其他问题很少,但我无法直接在我的代码中实现。

我试图检查Zoom Canvas to Mouse Cursor 中提供的demo Phrogz,但它太复杂了(至少对我而言)。尝试实施他的解决方案:

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);
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;

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


更新 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);
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;

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

【问题讨论】:

  • 看看这个之前的回答here
  • @MonicaOlejniczak 是的,已经看到了。当您发表评论时,我正在更新问题。他的演示太复杂,无法完美实现他的解决方案。
  • 在放大/缩小时,如果您将画布缓存到内存中的第二个画布并使用该第二个画布作为源在主画布上进行绘制,您将获得更好的性能。不缩放时,您直接使用主画布进行绘图。这是之前的Q&A,展示了如何放大鼠标位置。
  • @markE 这仍然适用于动画画布吗?我最终会缩放画布动画。只是在尝试atm。只要缩放原点固定,更新后的代码似乎可以很好地放大/缩小。缩放原点(鼠标指针位置)的微小变化会导致“跳跃”。 Phrogz 的演示似乎没有这个问题。
  • 缩放固定画布内容(使用第二个内存画布),是的。缩放不断变化的画布内容——谁知道呢,你必须测试你的应用才能看到它的行为。

标签: javascript animation canvas zooming


【解决方案1】:

如果只是缩放和平移,那么解决方法很简单。

您需要跟踪两个来源。一种是鼠标在世界坐标中的位置(框和圆的位置),另一种是鼠标在屏幕坐标中的位置(画布像素)

您需要习惯于从一种坐标系转换到另一种坐标系。这是通过反函数完成的。从世界坐标到屏幕坐标可以通过将屏幕坐标转换为世界坐标的反函数进行反转。

一些简单函数取反的例子

  • 2 * 10 = 20 倒数是 20 / 10 = 2
  • 2 + 3 = 5 倒数是 5 - 3 = 2
  • (3 - 1) * 5 = 10 倒数是 10 * (1/5) + 1 = 3

乘以* 1。即x*5 变为x * 1/5(或只是x/5) 加法变为减法,减法变为加法,先到后,后到先(3 - first) * last = result,反之为result / last + first = 3

因此,您缩放坐标(框的世界坐标位置)并以像素为单位获取框的屏幕位置。如果你想要一个屏幕像素的世界坐标,你可以应用逆向

这都是满口的,所以这里是你的代码用一些 cmets 做你需要的事情,我添加了 mousemove、button 的东西和其他东西,因为你需要鼠标 pos,如果你不能平移,就没有缩放的意义,需要停止鼠标按钮锁定并停止滚轮滚动等等等等...要平移只需移动世界原点(在代码中)单击 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);
}
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;

【讨论】:

  • 我喜欢这个。谢谢你。我已经完成了平移效果,但你已经将其中两个结合在一起,这很棒。赞成,但首先,让我处理代码:)
  • 我已经isolated 平移效果,我想,我开始了解它是如何工作的。但是,我有一个问题。这可以使用 translatescale 来完成,就像在 Phrogz 的 demo 中一样,而不是缩放绘制对象的 x, y, width, height
  • @akinuri 首先处理这个可能是值得的。您可以使用矩阵来完成,过程大致相同。矩阵只是为所涉及的数学提供了一些捷径。这个例子stackoverflow.com/a/38676266/3877726 显示了一个更复杂的版本。(有时给图像时间加载它的速度很慢)有一个纯矩阵数学方法,但我还没有在这里发布类似的东西。
  • 你好 Blindman67。我希望如果我劫持这个评论部分没问题。几个月来我一直在开发一款游戏,但我在缩放功能方面遇到了很大的麻烦。或者,更具体地说,我的问题是:我如何跟踪已缩放的对象位置(即在画布上绘制对象原点 x/y,缩放 -> 我无法检查 mousemove 事件 x/y 与该对象不再存在,因为偏移量不再合适。请看一下这个完整的代码笔,我会帮我很多。花了这么多时间......codepen.io/AncientSion/pen/zZNqBr
  • @Blindman67,哪个是最快的选择? translatescale,矩阵或单独缩放画布的项目?
猜你喜欢
  • 2022-07-31
  • 2016-01-05
  • 2016-08-26
  • 1970-01-01
  • 2011-10-14
  • 2013-12-31
  • 2013-10-28
  • 2011-10-10
  • 2017-10-16
相关资源
最近更新 更多