How to bound image pan when zooming (HTML Canvas)

前端 未结 1 1474
情歌与酒
情歌与酒 2020-12-09 06:48

I\'m trying to limit boundaries and I\'m running into issues. I\'m upscaling an image from another canvas and then implementing zoom and pan. My issue (code below) is with

相关标签:
1条回答
  • 2020-12-09 07:42

    Using transformation matrix to constrain a view

    To constrain the position you need to transform the corner coordinates of the image to screen coordinates. As getting the transform is still not standard across browsers the demo below holds a copy of the transform.

    The object view holds the canvas view. When you use the function view.setBounds(top,left,right,bottom); the view will be locked to that area (the image you are viewing 0,0,100,100)

    The scale and position (origin) will be constrained to keep the bounds outside or one the edge of the canvas context set by view.setContext(context).

    The function scaleAt(pos,amount); will scale at a specified pos (eg mouse position)

    To set the transform use view.apply() this will update the view transform and set the context transform.

    The are a few other functions that may prove handy see code.

    Demo uses mouse click drag to pan and wheel to zoom.

    Demo is a copy of the OP's example width modifications to answer question.

    // use requestAnimationFrame when doing any form of animation via javascript
    requestAnimationFrame(draw);
    
    var zoomIntensity = 0.2;
    
    var canvas = document.getElementById("canvas");
    var canvas2 = document.getElementById("canvas2");
    var context = canvas.getContext("2d");
    var context2 = canvas2.getContext("2d");
    var width = 200;
    var height = 200;
    context.font = "24px arial";
    context.textAlign = "center";
    context.lineJoin = "round"; // to prevent miter spurs on strokeText 
    //fill smaller canvas with random pixels
    for(var x = 0; x < 100; x++){
      for(var y = 0; y < 100; y++) {
        var rando = function(){return Math.floor(Math.random() * 9)};
        var val = rando();
        if(x === 0 || y === 0 || x === 99 || y === 99){
            context2.fillStyle = "#FF0000";
        }else{
            context2.fillStyle = "#" + val + val + val;
        
        }
        context2.fillRect(x,y,1,1);
      }
    }
    
    // mouse holds mouse position button state, and if mouse over canvas with overid
    var mouse = {
        pos : {x : 0, y : 0},
        worldPos : {x : 0, y : 0},
        posLast : {x : 0, y : 0},
        button : false,
        overId : "",  // id of element mouse is over
        dragging : false,
        whichWheel : -1, // first wheel event will get the wheel
        wheel : 0,
    }
    
    // View handles zoom and pan (can also handle rotate but have taken that out as rotate can not be contrained without losing some of the image or seeing some of the background.
    const view = (()=>{
        const matrix = [1,0,0,1,0,0]; // current view transform
        const invMatrix = [1,0,0,1,0,0]; // current inverse view transform
        var m = matrix;  // alias
        var im = invMatrix; // alias
        var scale = 1;   // current scale
        const bounds = {
            topLeft : 0,
            left : 0,
            right : 200,
            bottom : 200,
        }
        var useConstraint = true; // if true then limit pan and zoom to 
                                  // keep bounds within the current context
        
        var maxScale = 1;
        const workPoint1 = {x :0, y : 0};
        const workPoint2 = {x :0, y : 0};
        const wp1 = workPoint1; // alias
        const wp2 = workPoint2; // alias
        var ctx;
        const pos = {      // current position of origin
            x : 0,
            y : 0,
        }
        var dirty = true;
        const API = {
            canvasDefault () { ctx.setTransform(1,0,0,1,0,0) },
            apply(){
                if(dirty){ this.update() }
                ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
            },
            getScale () { return scale },
            getMaxScale () { return maxScale },
            matrix,  // expose the matrix
            invMatrix, // expose the inverse matrix
            update(){ // call to update transforms
                dirty = false;
                m[3] = m[0] = scale;
                m[1] = m[2] = 0;
                m[4] = pos.x;
                m[5] = pos.y;
                if(useConstraint){
                    this.constrain();
                }
                this.invScale = 1 / scale;
                // calculate the inverse transformation
                var cross = m[0] * m[3] - m[1] * m[2];
                im[0] =  m[3] / cross;
                im[1] = -m[1] / cross;
                im[2] = -m[2] / cross;
                im[3] =  m[0] / cross;
            },
            constrain(){
                maxScale = Math.min(
                    ctx.canvas.width / (bounds.right - bounds.left) ,
                    ctx.canvas.height / (bounds.bottom - bounds.top)
                );
                if (scale < maxScale) {  m[0] = m[3] = scale = maxScale }
                wp1.x = bounds.left;
                wp1.y = bounds.top;
                this.toScreen(wp1,wp2);
                if (wp2.x > 0) { m[4] = pos.x -= wp2.x }
                if (wp2.y > 0) { m[5] = pos.y -= wp2.y }
                wp1.x = bounds.right;
                wp1.y = bounds.bottom;
                this.toScreen(wp1,wp2);
                if (wp2.x < ctx.canvas.width) { m[4] = (pos.x -= wp2.x -  ctx.canvas.width) }
                if (wp2.y < ctx.canvas.height) { m[5] = (pos.y -= wp2.y -  ctx.canvas.height) }
            
            },
            toWorld(from,point = {}){  // convert screen to world coords
                var xx, yy;
                if(dirty){ this.update() }
                xx = from.x - m[4];     
                yy = from.y - m[5];     
                point.x = xx * im[0] + yy * im[2]; 
                point.y = xx * im[1] + yy * im[3];
                return point;
            },        
            toScreen(from,point = {}){  // convert world coords to screen coords
                if(dirty){ this.update() }
                point.x =  from.x * m[0] + from.y * m[2] + m[4]; 
                point.y = from.x * m[1] + from.y * m[3] + m[5];
                return point;
            },        
            scaleAt(at, amount){ // at in screen coords
                if(dirty){ this.update() }
                scale *= amount;
                pos.x = at.x - (at.x - pos.x) * amount;
                pos.y = at.y - (at.y - pos.y) * amount;            
                dirty = true;
            },
            move(x,y){  // move is in screen coords
                pos.x += x;
                pos.y += y;
                dirty = true;
            },
            setContext(context){
                ctx = context;
                dirty = true;
            },
            setBounds(top,left,right,bottom){
                bounds.top = top;
                bounds.left = left;
                bounds.right = right;
                bounds.bottom = bottom;
                useConstraint = true;
                dirty = true;
            }
        };
        return API;
    })();
    view.setBounds(0,0,canvas2.width,canvas2.height);
    view.setContext(context); 
    
    
    
    //draw the larger canvas
    function draw(){
        view.canvasDefault(); // se default transform to clear screen
    		context.imageSmoothingEnabled = false;
        context.fillStyle = "white";
        context.fillRect(0, 0, width, height);
        view.apply();  // set the current view
    		context.drawImage(canvas2, 0,0);
        view.canvasDefault();
        if(view.getScale() === view.getMaxScale()){
           context.fillStyle = "black";
           context.strokeStyle = "white";
           context.lineWidth = 2.5;
           context.strokeText("Max scale.",context.canvas.width / 2,24);
           context.fillText("Max scale.",context.canvas.width / 2,24);
        }
        requestAnimationFrame(draw);
        if(mouse.overId === "canvas"){
            canvas.style.cursor = mouse.button ? "none" : "move";
        }else{
            canvas.style.cursor = "default";
        }
    }
    // add events to document so that mouse is captured when down on canvas
    // This allows the mouseup event to be heard no matter where the mouse has
    // moved to.
    "mousemove,mousedown,mouseup,mousewheel,wheel,DOMMouseScroll".split(",")
        .forEach(eventName=>document.addEventListener(eventName,mouseEvent));
    
    function mouseEvent (event){
        mouse.overId = event.target.id;
        if(event.target.id === "canvas" || mouse.dragging){ // only interested in canvas mouse events including drag event started on the canvas.
    
            mouse.posLast.x = mouse.pos.x;
            mouse.posLast.y = mouse.pos.y;    
            mouse.pos.x = event.clientX - canvas.offsetLeft;
            mouse.pos.y = event.clientY - canvas.offsetTop;    
            view.toWorld(mouse.pos, mouse.worldPos); // gets the world coords (where on canvas 2 the mouse is)
            if (event.type === "mousemove"){
                if(mouse.button){
                    view.move(
                       mouse.pos.x - mouse.posLast.x,
                       mouse.pos.y - mouse.posLast.y
                    )
                }
            } else if (event.type === "mousedown") { mouse.button = true; mouse.dragging = true }        
            else if (event.type === "mouseup") { mouse.button = false; mouse.dragging = false }
            else if(event.type === "mousewheel" && (mouse.whichWheel === 1 || mouse.whichWheel === -1)){
                mouse.whichWheel = 1;
                mouse.wheel = event.wheelDelta;
            }else if(event.type === "wheel" && (mouse.whichWheel === 2 || mouse.whichWheel === -1)){
                mouse.whichWheel = 2;
                mouse.wheel = -event.deltaY;
            }else if(event.type === "DOMMouseScroll" && (mouse.whichWheel === 3 || mouse.whichWheel === -1)){
                mouse.whichWheel = 3;
                mouse.wheel = -event.detail;
            }
            if(mouse.wheel !== 0){
                event.preventDefault();
                view.scaleAt(mouse.pos, Math.exp((mouse.wheel / 120) *zoomIntensity));
                mouse.wheel = 0;
            }
        }
    }
    div { user-select: none;}   /* mouse prevent drag selecting content */
    canvas { border:2px solid black;}
    <div>
    <canvas id="canvas" width="200" height="200"></canvas>
    <canvas id="canvas2" width="100" height="100"></canvas>
    <p>Mouse wheel to zoom. Mouse click drag to pan.</p>
    <p>Zoomed image constrained to canvas</p>
    </div>

    0 讨论(0)
提交回复
热议问题