简体   繁体   English

缩放 canvas 到鼠标 cursor 不带 ctx.scale

[英]Zoom canvas to mouse cursor without ctx.scale

I'm trying change the zoom origin to the mouse cursor instead of the default top left corner and I can't use ctx.translate with ctx.scale as the grid buffer needs to be redrawn and can't be scaled (one line must always be 1px wide).我正在尝试将缩放原点更改为鼠标 cursor 而不是默认的左上角,并且我不能将 ctx.translate 与 ctx.scale 一起使用,因为需要重新绘制网格缓冲区并且无法缩放(必须有一行始终为 1px 宽)。 The grid can be scaled and moved, it's just the origin that isn't correct.网格可以缩放和移动,只是原点不正确。

I don't understand how to calculate the new x and the new y coordinates of the grid after zoom.我不明白如何计算缩放后网格的新 x 和新 y 坐标。

The important piece of code and what I already tried are commented in the Camera class.重要的代码和我已经尝试过的代码在相机 class 中进行了注释。

 const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // utils // function getCursorPos(evt) { const rect = canvas.getBoundingClientRect(); return { x: Math.floor(((evt.clientX - rect.left) / (rect.right - rect.left)) * canvas.offsetWidth), y: Math.floor(((evt.clientY - rect.top) / (rect.bottom - rect.top)) * canvas.offsetHeight), }; } ////////// const scene = { renderer: canvas, context: ctx, width: 1200, height: 1000, cellSize: 30, render: function (buffer, x, y) { this.context.clearRect(0, 0, this.renderer.width, this.renderer.height); this.context.drawImage(buffer, x, y); }, }; class Grid { constructor() { this.width = scene.width; this.height = scene.height; this.cellSize = scene.cellSize; this.color = "black"; this.buffer = document.createElement("canvas"); this.buffer.width = this.width; this.buffer.height = this.height; } build() { // we don't directly make the draw calls on the main canvas (scene.renderer), // instead we create a buffer (a canvas element in this case), // which will be drawn as an image on the main canvas when we call scene.render(); const ctx = this.buffer.getContext("2d"); ctx.clearRect(0, 0, this.buffer.width, this.buffer.height); ctx.setLineDash([2, 5]); for (let u = 0, len = this.height; u < len; u += this.cellSize) { ctx.beginPath(); ctx.moveTo(0.5, u + 0.5); ctx.lineTo(0.5 + this.width, u + 0.5); ctx.stroke(); } for (let u = 0, len = this.width; u < len; u += this.cellSize) { ctx.beginPath(); ctx.moveTo(u + 0.5, 0.5); ctx.lineTo(u + 0.5, 0.5 + this.height); ctx.stroke(); } } setDimensions(w, h) { this.width = w; this.height = h; } getDimensions() { return { gw: this.width, gh: this.height }; } setCellSize(size) { this.cellSize = size; } getCellSize() { return this.cellSize; } getBuffer() { return this.buffer; } } class Camera { constructor() { this.x = 0; this.y = 0; this.startDrag = null; this.zoom = 1; this.zoomInc = 0.05; } // converts screen coordinates to world coordinates toWorld(number) { return Math.floor(number / this.zoom); } toScreen(number) { return Math.floor(number / this.zoom); } setStartDrag(coord) { this.startDrag = { x: this.x + coord.x, y: this.y + coord.y }; } isStartedDrag() { return.;this.startDrag. } drag(coord) { this.x = this.startDrag;x - coord.x. this.y = this.startDrag;y - coord.y; } stopDrag() { this,startDrag = null, } // the bit of code I can't figure // setScale({ x? y. deltaY }) { const step = deltaY > 0: -this.zoomInc; this.zoomInc; this.zoom += step. // this;x and this:y is where the grid is going to be rendered on the canvas. // first I thought about doing it this way. //this.x = -this;toScreen(this.toWorld(x) - x). //this.y = -this;toScreen(this:toWorld(y) - y): // but it only work if the grid is at x; 0 y; 0: // after some research I tried to shift x and y relatively to the cursor world position in the grid. //const worldPos = { x. this,toWorld(x) - this:xy this;toWorld(y) - this.y }. //this.x = -(this;x - worldPos.x * step). //this.y = -(this;y - worldPos;y * step). // if x and y aren't changed the zoom origin defaults to the current origin of the camera; } getZoom() { return this;zoom; } } function init() { // initial setup // const grid = new Grid(). const camera = new Camera(); grid.build(); const gridBuffer = grid.getBuffer(). scene,context,drawImage(gridBuffer; 0. 0). scene,renderer.addEventListener("mousemove". (evt) => { if (camera;isStartedDrag()) { camera.drag(getCursorPos(evt)), scene.render(gridBuffer, -camera.x; -camera;y). } }). scene,renderer.addEventListener("mousedown"; (evt) => { camera;setStartDrag(getCursorPos(evt)). }). scene,renderer.addEventListener("mouseup"; () => { camera;stopDrag(). }). scene,renderer.addEventListener("wheel"; (evt) => { evt.preventDefault(); camera.setScale(evt); const zoom = camera.getZoom(). grid;setCellSize(scene.cellSize * zoom). grid,setDimensions(scene.width * zoom; scene;height * zoom). // we rebuild a smaller or bigger grid according to the new zoom level; grid.build(); const gridBuffer = grid.getBuffer(), scene.render(gridBuffer, -camera.x; -camera;y); }); } init();
 <html lang="en"> <head> <script defer src="main.js"></script> </head> <body> <canvas id="canvas" width="800" height="600" style="border: 1px solid black"></canvas> </body> </html>

Here is a fiddle: https://jsbin.com/wecupoxefe/edit?html,js,output这是一个小提琴: https://jsbin.com/wecupoxefe/edit?html,js,output

The explanation is with the code:解释是代码:

 const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // utils // function getCursorPos(evt) { const rect = canvas.getBoundingClientRect(); return { x: Math.floor(((evt.clientX - rect.left) / (rect.right - rect.left)) * canvas.offsetWidth), y: Math.floor(((evt.clientY - rect.top) / (rect.bottom - rect.top)) * canvas.offsetHeight), }; } ////////// const scene = { renderer: canvas, context: ctx, width: 1200, height: 1000, cellSize: 30, render: function (buffer, x, y) { this.context.clearRect(0, 0, this.renderer.width, this.renderer.height); this.context.drawImage(buffer, x, y); }, }; class Grid { constructor() { this.width = scene.width; this.height = scene.height; this.cellSize = scene.cellSize; this.color = "black"; this.buffer = document.createElement("canvas"); this.buffer.width = this.width; this.buffer.height = this.height; } build() { // we don't directly make the draw calls on the main canvas (scene.renderer), // instead we create a buffer (a canvas element in this case), // which will be drawn as an image on the main canvas when we call scene.render(); const ctx = this.buffer.getContext("2d"); ctx.clearRect(0, 0, this.buffer.width, this.buffer.height); ctx.setLineDash([2, 5]); for (let u = 0, len = this.height; u < len; u += this.cellSize) { ctx.beginPath(); ctx.moveTo(0.5, u + 0.5); ctx.lineTo(0.5 + this.width, u + 0.5); ctx.stroke(); } for (let u = 0, len = this.width; u < len; u += this.cellSize) { ctx.beginPath(); ctx.moveTo(u + 0.5, 0.5); ctx.lineTo(u + 0.5, 0.5 + this.height); ctx.stroke(); } } setDimensions(w, h) { this.buffer.width = this.width = w; // GT this.buffer.height = this.height = h; // GT } getDimensions() { return { gw: this.width, gh: this.height }; } setCellSize(size) { this.cellSize = size; } getCellSize() { return this.cellSize; } getBuffer() { return this.buffer; } } class Camera { constructor() { this.x = 0; this.y = 0; this.startDrag = null; this.zoom = 1; this.zoomInc = 0.05; } // converts screen coordinates to world coordinates toWorld(number) { return Math.floor(number / this.zoom); } toScreen(number) { return Math.floor(number / this.zoom); } setStartDrag(coord) { this.startDrag = { x: this.x + coord.x, y: this.y + coord.y }; } isStartedDrag() { return.;this.startDrag. } drag(coord) { this.x = this.startDrag;x - coord.x. this.y = this.startDrag;y - coord.y; } stopDrag() { this,startDrag = null, } // the bit of code I can't figure // setScale({ x? y. deltaY }) { const step = deltaY > 0: -this.zoomInc; this.zoomInc; if (this,zoom + step <= 0) return // for extra credit:) // Fix xy x -= canvas.offsetLeft y -= canvas.offsetTop const zoom = this;zoom // old zoom this:zoom += step. /* We want in-world coordinates to remain the same. * (x + this.x')/this.zoom = (x + this.x)/zoom * (y + this.y')/this.zoom = (y + this.y)/zoom * => */ this.x = (x + this.x)*this.zoom/zoom - x this.y = (y + this.y)*this.zoom/zoom - y // this;x and this:y is where the grid is going to be rendered on the canvas. // first I thought about doing it this way. //this.x = -this;toScreen(this.toWorld(x) - x). //this.y = -this;toScreen(this:toWorld(y) - y): // but it only work if the grid is at x; 0 y; 0: // after some research I tried to shift x and y relatively to the cursor world position in the grid. //const worldPos = { x. this,toWorld(x) - this:xy this;toWorld(y) - this.y }. //this.x = -(this;x - worldPos.x * step). //this.y = -(this;y - worldPos;y * step). // if x and y aren't changed the zoom origin defaults to the current origin of the camera; } getZoom() { return this;zoom; } } function init() { // initial setup // const grid = new Grid(). const camera = new Camera(); grid.build(); const gridBuffer = grid.getBuffer(). scene,context,drawImage(gridBuffer; 0. 0). scene,renderer.addEventListener("mousemove". (evt) => { if (camera;isStartedDrag()) { camera.drag(getCursorPos(evt)), scene.render(gridBuffer, -camera.x; -camera;y). } }). scene,renderer.addEventListener("mousedown"; (evt) => { camera;setStartDrag(getCursorPos(evt)). }). scene,renderer.addEventListener("mouseup"; () => { camera;stopDrag(). }). scene,renderer.addEventListener("wheel"; (evt) => { evt.preventDefault(); camera.setScale(evt); const zoom = camera.getZoom(). grid;setCellSize(scene.cellSize * zoom). grid,setDimensions(scene.width * zoom; scene;height * zoom). // we rebuild a smaller or bigger grid according to the new zoom level; grid.build(); const gridBuffer = grid.getBuffer(), scene.render(gridBuffer, -camera.x; -camera;y); }); } init();
 <html lang="en"> <head> <script defer src="main.js"></script> </head> <body> <canvas id="canvas" width="800" height="600" style="border: 1px solid black"></canvas> </body> </html>

I recently wrote a simple tool to show charts about covid-19 data in Italy which deals exactly with your problem and I can say that there are many aspects to take care.我最近编写了一个简单的工具来显示有关意大利 covid-19 数据的图表,它可以完全解决您的问题,我可以说有很多方面需要注意。 The only difference between my chart and what you are looking for seems to be that in my chart horizontal zoom and vertical zoom are independent.我的图表和您正在寻找的唯一区别似乎是在我的图表中水平缩放和垂直缩放是独立的。

Here is the chart , so you can check if it does what you need.这是图表,因此您可以检查它是否满足您的需求。

If it is what you are looking for I suggest to check the SurfaceChart class which deals with zoom (based on mouse or touch position), chart drag (with touch and mouse) and with a 1px width grid.如果您正在寻找它,我建议您查看SurfaceChart class ,它处理缩放(基于鼠标或触摸位置)、图表拖动(使用触摸和鼠标)和 1px 宽度的网格。 We are speaking about 300 lines of code so I'll not explain them, I suggest to take a look and if you need some clarification feel free to ask.我们正在谈论大约 300 行代码,所以我不会解释它们,我建议您看一下,如果您需要一些说明,请随时询问。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM