I would like to “zoom” (mouse wheel) a grid I have, around the mouse position in canvas (like Desmos does). By zooming, I mean redrawing the lines inside the canvas to look like a zoom effect, NOT performing an actual zoom. And I would like to only use vanilla javascript and no libraries (so that I can learn).
At this point, I set up a very basic magnification effect that only multiplies the distance between the gridlines, based on the mouse wheel values.
//////////////////////////////////////////////////////////////////////////////// // User contants const canvasWidth = 400; const canvasHeight = 200; const canvasBackground = '#282c34'; const gridCellColor = "#777"; const gridBlockColor = "#505050"; const axisColor = "white"; // Internal constants const canvas = document.getElementById('canvas'); const context = canvas.getContext('2d', { alpha: false }); const bodyToCanvas = 8; //////////////////////////////////////////////////////////////////////////////// // User variables let cellSize = 10; let cellBlock = 5; let xSubdivs = 40 let ySubdivs = 20 // Internal variables let grid = ''; let zoom = 0; let xAxisOffset = xSubdivs/2; let yAxisOffset = ySubdivs/2; let mousePosX = 0; let mousePosY = 0; //////////////////////////////////////////////////////////////////////////////// // Classes class Grid{ constructor() { this.width = canvasWidth, this.height = canvasHeight, this.cellSize = cellSize, this.cellBlock = cellBlock, this.xSubdivs = xSubdivs, this.ySubdivs = ySubdivs } draw(){ // Show canvas context.fillStyle = canvasBackground; context.fillRect(-this.width/2, -this.height/2, this.width, this.height); // Horizontal lines this.xSubdivs = Math.floor(this.height / this.cellSize); for (let i = 0; i <= this.xSubdivs; i++) {this.setHorizontalLines(i);} // Vertical lines this.ySubdivs = Math.floor(this.width / this.cellSize); for (let i = 0; i <= this.ySubdivs; i++) {this.setVerticalLines(i) ;} // Axis this.setAxis(); } setHorizontalLines(i) { // Style context.lineWidth = 0.5; if (i % this.cellBlock == 0) { // light lines context.strokeStyle = gridCellColor; } else{ // Dark lines context.strokeStyle = gridBlockColor; } //Draw lines context.beginPath(); context.moveTo(-this.width/2, (this.cellSize * i) - this.height/2); context.lineTo( this.width/2, (this.cellSize * i) - this.height/2); context.stroke(); context.closePath(); } setVerticalLines(i) { // Style context.lineWidth = 0.5; if (i % cellBlock == 0) { // Light lines context.strokeStyle = gridCellColor; } else { // Dark lines context.strokeStyle = gridBlockColor; } //Draw lines context.beginPath(); context.moveTo((this.cellSize * i) - this.width/2, -this.height/2); context.lineTo((this.cellSize * i) - this.width/2, this.height/2); context.stroke(); context.closePath(); } // Axis are separated from the line loops so that they remain on // top of them (cosmetic measure) setAxis(){ // Style x Axis context.lineWidth = 1.5; context.strokeStyle = axisColor; // Draw x Axis context.beginPath(); context.moveTo(-this.width/2, (this.cellSize * yAxisOffset) - this.height/2); context.lineTo( this.width/2, (this.cellSize * yAxisOffset) - this.height/2); context.stroke(); context.closePath(); // Style y axis context.lineWidth = 1.5; context.strokeStyle = axisColor; // Draw y axis context.beginPath(); context.moveTo((this.cellSize * xAxisOffset ) - this.width/2, -this.height/2); context.lineTo((this.cellSize * xAxisOffset ) - this.width/2, this.height/2); context.stroke(); context.closePath(); } } //////////////////////////////////////////////////////////////////////////////// // Functions function init() { // Set up canvas if (window.devicePixelRatio > 1) { canvas.width = canvasWidth * window.devicePixelRatio; canvas.height = canvasHeight * window.devicePixelRatio; context.scale(window.devicePixelRatio, window.devicePixelRatio); } else { canvas.width = canvasWidth; canvas.height = canvasHeight; } canvas.style.width = canvasWidth + "px"; canvas.style.height = canvasHeight + "px"; // Initialize coordinates in the middle of the canvas context.translate(canvasWidth/2,canvasHeight/2) // Setup the grid grid = new Grid(); // Display the grid grid.draw(); } function setZoom(){ grid.cellSize = grid.cellSize + zoom; grid.draw(); } //////////////////////////////////////////////////////////////////////////////// //Launch the page init(); //////////////////////////////////////////////////////////////////////////////// // Update the page on resize window.addEventListener("resize", init); // Zoom the canvas with mouse wheel canvas.addEventListener('mousewheel', function (e) { e.preventDefault(); e.stopPropagation(); zoom = e.wheelDelta/120; requestAnimationFrame(setZoom); }) // Get mouse coordinates on mouse move. canvas.addEventListener('mousemove', function (e) { e.preventDefault(); e.stopPropagation(); mousePosX = parseInt(e.clientX)-bodyToCanvas ; mousePosY = parseInt(e.clientY)-bodyToCanvas ; }) ////////////////////////////////////////////////////////////////////////////////
html, body { background:#21252b; width:100%; height:100%; margin:0px; padding:0px; overflow: hidden; } span{ color:white; font-family: arial; font-size: 12px; margin:8px; } #canvas{ margin:8px; border: 1px solid white; }
<span>Use mouse wheel to zoom</span> <canvas id="canvas"></canvas>
This works fine but, as expected, it magnifies the grid from the top left corner not from the mouse position.
So, I thought about detecting the mouse position and then modifying all the “moveTo” and “lineTo” parts. The goal would be to offset the magnified grid so that everything is displaced except the 2 lines intersecting the current mouse coordinates.
For instance, it feels to me that instead of this:
context.moveTo( (this.cellSize * i) - this.width/2, -this.height/2 );
I should have something like this:
context.moveTo( (this.cellSize * i) - this.width/2 + OFFSET BASED ON MOUSE COORDS, -this.height/2 );
But after hours of trials and errors, I’m stuck. So, any help would be appreciated.
FYI: I already coded a functional panning system that took me days to achieve (that I stripped from the code here for clarity), so I would like to keep most of the logic I have so far, if possible. Unless my code is total nonsense or has performance issues, of course.
Thank you.
Advertisement
Answer
You’re already tracking the mouse position in pixels. If you transform the mouse position to the coordinate system of your grid, you can define which grid cell it’s hovering.
After zooming in, you can again calculate the mouse’s coordinate. When zooming in around another center point, you’ll see the coordinate shift.
You can undo that shift by translating the grid’s center in the opposite direction.
Here’s the main part of it:
function setZoom() { // Calculate the mouse position before applying the zoom // in the coordinate system of the grid const x1 = (mousePosX - grid.centerX) / grid.cellSize; const y1 = (mousePosY - grid.centerY) / grid.cellSize; // Make the zoom happen: update the cellSize grid.cellSize = grid.cellSize + zoom; // After zoom, you'll see the coordinates changed const x2 = (mousePosX - grid.centerX) / grid.cellSize; const y2 = (mousePosY - grid.centerY) / grid.cellSize; // Calculate the shift const dx = x2 - x1; const dy = y2 - y1; // "Undo" the shift by shifting the coordinate system's center grid.centerX += dx * grid.cellSize; grid.centerY += dy * grid.cellSize; grid.draw(); }
To make this easier to work with, I introduced a grid.centerX
and .centerY
. I updated your draw
method to take the center in to account (and kind of butchered it along the way, but I think you’ll manage to improve that a bit).
Here’s the complete example:
//////////////////////////////////////////////////////////////////////////////// // User contants const canvasWidth = 400; const canvasHeight = 200; const canvasBackground = '#282c34'; const gridCellColor = "#777"; const gridBlockColor = "#505050"; const axisColor = "white"; // Internal constants const canvas = document.getElementById('canvas'); const context = canvas.getContext('2d', { alpha: false }); const bodyToCanvas = 8; //////////////////////////////////////////////////////////////////////////////// // User variables let cellSize = 10; let cellBlock = 5; let xSubdivs = 40 let ySubdivs = 20 // Internal variables let grid = ''; let zoom = 0; let xAxisOffset = xSubdivs/2; let yAxisOffset = ySubdivs/2; let mousePosX = 0; let mousePosY = 0; //////////////////////////////////////////////////////////////////////////////// // Classes class Grid{ constructor() { this.width = canvasWidth, this.height = canvasHeight, this.cellSize = cellSize, this.cellBlock = cellBlock, this.xSubdivs = xSubdivs, this.ySubdivs = ySubdivs, this.centerX = canvasWidth / 2, this.centerY = canvasHeight / 2 } draw(){ // Show canvas context.fillStyle = canvasBackground; context.fillRect(0, 0, this.width, this.height); // Horizontal lines const minIY = -Math.ceil(this.centerY / this.cellSize); const maxIY = Math.ceil((this.height - this.centerY) / this.cellSize); for (let i = minIY; i <= maxIY; i++) {this.setHorizontalLines(i);} // Vertical lines const minIX = -Math.ceil(this.centerX / this.cellSize); const maxIX = Math.ceil((this.width - this.centerX) / this.cellSize); for (let i = minIX; i <= maxIX; i++) {this.setVerticalLines(i) ;} this.setVerticalLines(0); this.setHorizontalLines(0); } setLineStyle(i) { if (i === 0) { context.lineWidth = 1.5; context.strokeStyle = axisColor; } else if (i % cellBlock == 0) { // Light lines context.lineWidth = 0.5; context.strokeStyle = gridCellColor; } else { // Dark lines context.lineWidth = 0.5; context.strokeStyle = gridBlockColor; } } setHorizontalLines(i) { // Style this.setLineStyle(i); //Draw lines const y = this.centerY + this.cellSize * i; context.beginPath(); context.moveTo(0, y); context.lineTo(this.width, y); context.stroke(); context.closePath(); } setVerticalLines(i) { // Style this.setLineStyle(i); //Draw lines const x = this.centerX + this.cellSize * i; context.beginPath(); context.moveTo(x, 0); context.lineTo(x, this.height); context.stroke(); context.closePath(); } } //////////////////////////////////////////////////////////////////////////////// // Functions function init() { // Set up canvas if (window.devicePixelRatio > 1) { canvas.width = canvasWidth * window.devicePixelRatio; canvas.height = canvasHeight * window.devicePixelRatio; context.scale(window.devicePixelRatio, window.devicePixelRatio); } else { canvas.width = canvasWidth; canvas.height = canvasHeight; } canvas.style.width = canvasWidth + "px"; canvas.style.height = canvasHeight + "px"; // Setup the grid grid = new Grid(); // Display the grid grid.draw(); } function setZoom() { // Calculate the mouse position before applying the zoom // in the coordinate system of the grid const x1 = (mousePosX - grid.centerX) / grid.cellSize; const y1 = (mousePosY - grid.centerY) / grid.cellSize; grid.cellSize = grid.cellSize + zoom; // After zoom, you'll see the coordinates changed const x2 = (mousePosX - grid.centerX) / grid.cellSize; const y2 = (mousePosY - grid.centerY) / grid.cellSize; // Calculate the shift const dx = x2 - x1; const dy = y2 - y1; // "Undo" the shift by shifting the coordinate system's center grid.centerX += dx * grid.cellSize; grid.centerY += dy * grid.cellSize; grid.draw(); } //////////////////////////////////////////////////////////////////////////////// //Launch the page init(); //////////////////////////////////////////////////////////////////////////////// // Update the page on resize window.addEventListener("resize", init); // Zoom the canvas with mouse wheel canvas.addEventListener('mousewheel', function (e) { e.preventDefault(); e.stopPropagation(); zoom = e.wheelDelta/120; requestAnimationFrame(setZoom); }) // Get mouse coordinates on mouse move. canvas.addEventListener('mousemove', function (e) { e.preventDefault(); e.stopPropagation(); mousePosX = parseInt(e.clientX)-bodyToCanvas ; mousePosY = parseInt(e.clientY)-bodyToCanvas ; }) ////////////////////////////////////////////////////////////////////////////////
html, body { background:#21252b; width:100%; height:100%; margin:0px; padding:0px; overflow: hidden; } span{ color:white; font-family: arial; font-size: 12px; margin:8px; } #canvas{ margin:8px; border: 1px solid white; }
<canvas id="canvas"></canvas>