Wednesday, 6 July 2022

Javascript pseudo-zoom around mouse position in canvas

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.



from Javascript pseudo-zoom around mouse position in canvas

No comments:

Post a Comment