TMG TMG - 1 month ago 22
Javascript Question

Viewport from bigger image with html5 canvas

I'm trying to use a smaller canvas as a viewport for another much bigger canvas.
I really like the approach used in this the solution on a similar question.

He basically uses

CanvasRenderingContext2D.drawImage()
to "crop" the buffer/offset canvas and then displays that portion of the image on the smaller canvas (viewport). I'm trying to implement a simpler version of the given solution in this fiddle: https://jsfiddle.net/avvac0x8/2/. But as you can see the viewport is not entirely in sync with the big image (and vice-versa).

It seems that the after mentioned solution does not work with different canvas sizes. So I need to make it "canvas-size agnostic".

Maybe I'm missing some kind of scalling calculations, but I don't know how to go from here, any tips are welcome.

EDIT:

Updated the fiddle to work properly: https://jsfiddle.net/avvac0x8/4/. Aparently the original image should not be scaled to fit the buffer canvas. What it should be done instead is that the offset/buffer canvas should be the same size as the original image.

Answer

The simplest way is to use another canvas as a middle layer.

Here I will ignore the offset canvas because it is not needed unless you want to display the entire map. Presumably all you need is the zoomed in region. If you want to zoom out, you can simply draw the full image to your viewport window ( by providing the width and height parameters to ctx.drawImage ( img, x, y, viewPort.width, viewPort.height ) ). However you want to be sure that your image is manually cropped to an appropriate size so that the image does not appear stretched OR make sure that your canvas viewport is of the same aspect ratio as the image you are using.

The below works if you want the clipping region of the background ( the actual viewing area ) to be a different size (smaller or larger) than your viewport window ( the zoomed in/out viewing area ). Note that this is independent of how much larger or smaller the actual background is. Presumably both the clipped area and the viewport window are smaller than the background image itself.

For example:

// use these to determine where the clipping region lives
var offsetX = 0,
    offsetY = 0,
    clipWidth = <<yourChosenPixelWidth>>,
    clipHeight = <<yourChosenPixelHeight>>,
    clip = document.createElement ( "canvas" ),
    clipCtx,
    viewPort = document.getElementById ( "main-canvas" ),
    viewCtx = viewPort.getContext ( "2d" ),
    background = new Image (),
    // offsetCanvas = document.getElementById ( "offset-canvas" ),
    imgLoaded = false;

// configure the offset canvas once
background.src = "http://pixeljoint.com/files/icons/full/map__r1470206141.png";
background.onLoad = function() {
    // the fiddle scales the image, here we don't
    //offsetCanvas.width = background.width;
    //offsetCanvas.height = background.height;
    //offsetCtx = offsetCanvas.getContext ( "2d" );
    //offsetCtx.drawImage ( background, 0, 0 );
    imgLoaded = true;
}

clip.width = clipWidth;
clip.height = clipHeight;
clipCtx = clip.getContext ( "2d" );

function updateViewport () {
    if ( imgLoaded ) {
        // copy pixels from the background directly
        // to the middle layer so we have a "clipped"
        // but unscaled image object
        //clipCtx.putImageData ( offsetCtx.getImageData ( offsetX, offsetY, clip.width, clip.height ) );
        clipCtx.drawImage ( background, offsetX, offsetY );

        // this is where rescaling happens
        viewCtx.drawImage ( clip, 0, 0, viewPort.width, viewPort.height );

        // and you're done!
    }
}