Javascript pseudo-zoom around mouse position in canvas

I would like to “zoom” a grid I have, around the mouse position in canvas. By zooming, I mean redrawing the lines inside the canvas to look like a zoom effect, not performing an actual zoom (I don’t want that).

At this point, I have set a very basic magnification effect that only multiplies the distance between the gridlines, based on the mouse wheel values ​​while (updating the amount of rendered gridlines zooming is not implemented yet but that won’t be a problem).

But, of course, it magnifies the grid from the top left corner. It seems that zooming from the mouse position would require another strategy. But I don’t know how to achieve that since it’s not obvious.

I thought about detecting the mouse position and performing a radial zoom around that point but that seems quite mathy and somehow overkill. So, I wonder if there is another simple solution I didn’t think about.

So, what should I do to fit my zoom needs, according to my code? Are there easy ways to get there with what I already have or should I start fresh ?

I already coded a functional panning system that took me days to achieve (that I stripped from the code here for clarity), which is why I would like to keep most of the code logic I have so far, if possible.

Thank you.

////////////////////////////////////////////////////////////////////////////////////////

// Constantes Internes

const canvas           = document.getElementById('canvas');
const context          = canvas.getContext('2d', { alpha: false });
const bodyToCanvas     = 40;
const zoomConstant     = 0.0015;

// Constantes utilisateur

const canvasWidth      = 500;
const canvasHeight     = 300;
const canvasBackground = '#282c34';
const gridCellColor    = "#777";
const gridBlockColor   = "#505050";
const axisColor        = "white";

////////////////////////////////////////////////////////////////////////////////////////

// Variables  Internes

let grid      = '';
let zoom      = 0;
let mousePosX = 0;
let mousePosY = 0;

// Variables  utilisateur

let cellSize  = 10;
let cellBlock = 5;
let xSubdivs  = 25;
let ySubdivs  = 15;
let zoomSpeed = 5;

////////////////////////////////////////////////////////////////////////////////////////

// Classes

class Grid{
    constructor() {
        this.width      = canvasWidth,
        this.height     = canvasHeight,
        this.xSubdivs   = xSubdivs,
        this.ySubdivs   = ySubdivs,
        this.cellSize   = cellSize,
        this.cellBlock  = cellBlock,
        this.numHorizontalLines  = Math.floor(this.height / this.cellSize),
        this.numVerticalLines    = Math.floor(this.width  / this.cellSize);
        this.xNumStart  = {number: 1, suffix: ''},
        this.yNumStart  = {number: 1, suffix: ''}
    }

    draw(){
        // Afficher le canevas
        context.fillStyle = canvasBackground;
        context.fillRect(-this.width/2, -this.height/2, this.width, this.height);

        // Lignes horizontales
        this.numHorizontalLines = Math.floor(this.height / this.cellSize);
        for (let i = 0; i <= this.numHorizontalLines; i++) {this.setHorizontalLines(i);}

        // Lignes Verticales
        this.numVerticalLines   = Math.floor(this.width  / this.cellSize);
        for (let i = 0; i <= this.numVerticalLines; i++) {this.setVerticalLines(i)  ;}

         // Axes
        this.setAxis();
    }

    setHorizontalLines(i) {
        // Styler
        if (i % this.cellBlock == 0) {
            // Lignes traits clairs
            context.lineWidth = 0.5;
            context.strokeStyle = gridCellColor;
        }
        else if (i % cellBlock != 0){
            // Lignes traits foncés
            context.lineWidth = 0.5;
            context.strokeStyle = gridBlockColor;
        }

        //Tracer la ligne
        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) {
        // Styler
        if (i % cellBlock == 0) {
            // Lignes traits clairs
            context.lineWidth = 0.5;
            context.strokeStyle = gridCellColor;
        }
        else {
            // Lignes traits foncés
            context.lineWidth = 0.5;
            context.strokeStyle = gridBlockColor;
        }

        //Tracer la ligne
        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();
    }
    
    setAxis(){
        // Styler Axe des x 
        context.lineWidth = 1.5;
        context.strokeStyle = axisColor;

        // Tracer Axe des x 
        context.beginPath();
        context.moveTo(-this.width/2, (this.cellSize * this.ySubdivs) - this.height/2);
        context.lineTo( this.width/2, (this.cellSize * this.ySubdivs) - this.height/2);
        context.stroke();
        context.closePath();

         // Styler Axe des y
        context.lineWidth = 1.5;
        context.strokeStyle = axisColor;

        // Tracer Axe des y
        context.beginPath();
        context.moveTo((this.cellSize * this.xSubdivs) - this.width/2, -this.height/2);
        context.lineTo((this.cellSize * this.xSubdivs) - this.width/2,  this.height/2);
        context.stroke();
        context.closePath();
    }
}

////////////////////////////////////////////////////////////////////////////////////////

// Fonctions

function init() {
    // Préparer le canevas
    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";

    // Initialiser les coordonnées au milieu du canevas
    context.translate(canvasWidth/2,canvasHeight/2)

    // Préparer la grille
    grid = new Grid(); 

    // Afficher la grille
    grid.draw();
}

function setZoom(){
    grid.cellSize = grid.cellSize + (zoom * zoomConstant * zoomSpeed)
    grid.draw();
}

////////////////////////////////////////////////////////////////////////////////////////

//Lancement de la page

init();

////////////////////////////////////////////////////////////////////////////////////////

// Rafraichissement de la page

window.addEventListener("resize", init);

// Zoomer le canevas à la souris

canvas.addEventListener('mousewheel', function (e) {
    e.preventDefault();
    e.stopPropagation();
    zoom = e.wheelDelta;
    requestAnimationFrame(setZoom);
})

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;
}

#canvas{
    margin:40px;
    border: 1px solid white;
}
<canvas id="canvas"></canvas>

Leave a Comment