Home > database >  HTML Canvas: CanvasRenderingContext2D transform requires (re)setting canvas size?
HTML Canvas: CanvasRenderingContext2D transform requires (re)setting canvas size?

Time:01-21

I've modified https://codepen.io/chengarda/pen/wRxoyB

A simple example of internally handling scrolling and zooming of canvas contents using mouse and touch events. No JQuery, no libraries, pure JS.

to use a fixed canvas size and replaced the references to window.innerWidth (and Height) accordingly. The working code resets the canvas size on each draw - removing this causes the code to stop working (current Chrome and Firefox on Ubuntu), and I don't see why. Does setting the canvas size reset context or what is the (side) effect of doing it?

let canvas = document.getElementById("canvas")
canvas.width = 300;//window.innerWidth
canvas.height = 300;//window.innerHeight
let ctx = canvas.getContext('2d')

let cameraOffset = { x: canvas.width/2, y: canvas.height/2 }
let cameraZoom = 1
let MAX_ZOOM = 5
let MIN_ZOOM = 0.1
let SCROLL_SENSITIVITY = 0.0005

function draw()
{
    //canvas.width = window.innerWidth
    //canvas.height = window.innerHeight
    
    // Translate to the canvas centre before zooming - so you'll always zoom on what you're looking directly at
    ctx.translate( canvas.width / 2, canvas.height / 2 )
    ctx.scale(cameraZoom, cameraZoom)
    ctx.translate( -canvas.width / 2   cameraOffset.x, -canvas.height / 2   cameraOffset.y )
    ctx.clearRect(0,0, canvas.width, canvas.height)
    ctx.fillStyle = "#991111"
    drawRect(-50,-50,100,100)
    
    ctx.fillStyle = "#eecc77"
    drawRect(-35,-35,20,20)
    drawRect(15,-35,20,20)
    drawRect(-35,15,70,20)
    
    ctx.fillStyle = "#fff"
    drawText("Simple Pan and Zoom Canvas", -255, -100, 32, "courier")
    
    ctx.rotate(-31*Math.PI / 180)
    ctx.fillStyle = `#${(Math.round(Date.now()/40)@96).toString(16)}`
    drawText("Now with touch!", -110, 100, 32, "courier")
    
    ctx.fillStyle = "#fff"
    ctx.rotate(31*Math.PI / 180)
    
    drawText("Wow, you found me!", -260, -2000, 48, "courier")
    
    requestAnimationFrame( draw )
}

// Gets the relevant location from a mouse or single touch event
function getEventLocation(e)
{
    if (e.touches && e.touches.length == 1)
    {
        return { x:e.touches[0].clientX, y: e.touches[0].clientY }
    }
    else if (e.clientX && e.clientY)
    {
        return { x: e.clientX, y: e.clientY }        
    }
}

function drawRect(x, y, width, height)
{
    ctx.fillRect( x, y, width, height )
}

function drawText(text, x, y, size, font)
{
    ctx.font = `${size}px ${font}`
    ctx.fillText(text, x, y)
}

let isDragging = false
let dragStart = { x: 0, y: 0 }

function onPointerDown(e)
{
    isDragging = true
    dragStart.x = getEventLocation(e).x/cameraZoom - cameraOffset.x
    dragStart.y = getEventLocation(e).y/cameraZoom - cameraOffset.y
}

function onPointerUp(e)
{
    isDragging = false
    initialPinchDistance = null
    lastZoom = cameraZoom
}

function onPointerMove(e)
{
    if (isDragging)
    {
        cameraOffset.x = getEventLocation(e).x/cameraZoom - dragStart.x
        cameraOffset.y = getEventLocation(e).y/cameraZoom - dragStart.y
    }
}

function handleTouch(e, singleTouchHandler)
{
    if ( e.touches.length == 1 )
    {
        singleTouchHandler(e)
    }
    else if (e.type == "touchmove" && e.touches.length == 2)
    {
        isDragging = false
        handlePinch(e)
    }
}

let initialPinchDistance = null
let lastZoom = cameraZoom

function handlePinch(e)
{
    e.preventDefault()
    
    let touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY }
    let touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY }
    
    // This is distance squared, but no need for an expensive sqrt as it's only used in ratio
    let currentDistance = (touch1.x - touch2.x)**2   (touch1.y - touch2.y)**2
    
    if (initialPinchDistance == null)
    {
        initialPinchDistance = currentDistance
    }
    else
    {
        adjustZoom( null, currentDistance/initialPinchDistance )
    }
}

function adjustZoom(zoomAmount, zoomFactor)
{
    if (!isDragging)
    {
        if (zoomAmount)
        {
            cameraZoom  = zoomAmount
        }
        else if (zoomFactor)
        {
            console.log(zoomFactor)
            cameraZoom = zoomFactor*lastZoom
        }
        
        cameraZoom = Math.min( cameraZoom, MAX_ZOOM )
        cameraZoom = Math.max( cameraZoom, MIN_ZOOM )
        
        console.log(zoomAmount)
    }
}

canvas.addEventListener('mousedown', onPointerDown)
canvas.addEventListener('touchstart', (e) => handleTouch(e, onPointerDown))
canvas.addEventListener('mouseup', onPointerUp)
canvas.addEventListener('touchend',  (e) => handleTouch(e, onPointerUp))
canvas.addEventListener('mousemove', onPointerMove)
canvas.addEventListener('touchmove', (e) => handleTouch(e, onPointerMove))
canvas.addEventListener( 'wheel', (e) => adjustZoom(e.deltaY*SCROLL_SENSITIVITY))

// Ready, set, go
draw()
html, body
{
    height: 100%;
    margin: 0;
    padding: 0px;
    overflow: hidden;
}

#canvas
{
 /* width: 100%;
  height: 100%;*/
  background: #111;
}
<canvas id="canvas"></canvas>

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D

CodePudding user response:

Indeed, setting either the width or the height of your <canvas> will reset your context, all its attributes, and its buffer.

There is a new ctx.reset() method that does the same, but I believe it's an overkill: indeed, at least in Chrome both this method and the size change will reallocate a new bitmap buffer, which can leave a huge footprint on the memory consumption.

All you need in your case is to reset the context's Current Transform Matrix, and clear the buffer, you can do so in two calls:

ctx.setTransform({}); // equivalent to ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, canvas.width, canvas.height);

Add these two lines at the beginning of your drawing function and you're good to go.

let canvas = document.getElementById("canvas")
canvas.width = 300;//window.innerWidth
canvas.height = 300;//window.innerHeight
let ctx = canvas.getContext('2d')

let cameraOffset = { x: canvas.width/2, y: canvas.height/2 }
let cameraZoom = 1
let MAX_ZOOM = 5
let MIN_ZOOM = 0.1
let SCROLL_SENSITIVITY = 0.0005

function draw()
{
    ctx.setTransform({});
    ctx.clearRect(0,0, canvas.width, canvas.height)
    
    // Translate to the canvas centre before zooming - so you'll always zoom on what you're looking directly at
    ctx.translate( canvas.width / 2, canvas.height / 2 )
    ctx.scale(cameraZoom, cameraZoom)
    ctx.translate( -canvas.width / 2   cameraOffset.x, -canvas.height / 2   cameraOffset.y )
    ctx.clearRect(0,0, canvas.width, canvas.height)
    ctx.fillStyle = "#991111"
    drawRect(-50,-50,100,100)
    
    ctx.fillStyle = "#eecc77"
    drawRect(-35,-35,20,20)
    drawRect(15,-35,20,20)
    drawRect(-35,15,70,20)
    
    ctx.fillStyle = "#fff"
    drawText("Simple Pan and Zoom Canvas", -255, -100, 32, "courier")
    
    ctx.rotate(-31*Math.PI / 180)
    ctx.fillStyle = `#${(Math.round(Date.now()/40)@96).toString(16)}`
    drawText("Now with touch!", -110, 100, 32, "courier")
    
    ctx.fillStyle = "#fff"
    ctx.rotate(31*Math.PI / 180)
    
    drawText("Wow, you found me!", -260, -2000, 48, "courier")
    
    requestAnimationFrame( draw )
}

// Gets the relevant location from a mouse or single touch event
function getEventLocation(e)
{
    if (e.touches && e.touches.length == 1)
    {
        return { x:e.touches[0].clientX, y: e.touches[0].clientY }
    }
    else if (e.clientX && e.clientY)
    {
        return { x: e.clientX, y: e.clientY }        
    }
}

function drawRect(x, y, width, height)
{
    ctx.fillRect( x, y, width, height )
}

function drawText(text, x, y, size, font)
{
    ctx.font = `${size}px ${font}`
    ctx.fillText(text, x, y)
}

let isDragging = false
let dragStart = { x: 0, y: 0 }

function onPointerDown(e)
{
    isDragging = true
    dragStart.x = getEventLocation(e).x/cameraZoom - cameraOffset.x
    dragStart.y = getEventLocation(e).y/cameraZoom - cameraOffset.y
}

function onPointerUp(e)
{
    isDragging = false
    initialPinchDistance = null
    lastZoom = cameraZoom
}

function onPointerMove(e)
{
    if (isDragging)
    {
        cameraOffset.x = getEventLocation(e).x/cameraZoom - dragStart.x
        cameraOffset.y = getEventLocation(e).y/cameraZoom - dragStart.y
    }
}

function handleTouch(e, singleTouchHandler)
{
    if ( e.touches.length == 1 )
    {
        singleTouchHandler(e)
    }
    else if (e.type == "touchmove" && e.touches.length == 2)
    {
        isDragging = false
        handlePinch(e)
    }
}

let initialPinchDistance = null
let lastZoom = cameraZoom

function handlePinch(e)
{
    e.preventDefault()
    
    let touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY }
    let touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY }
    
    // This is distance squared, but no need for an expensive sqrt as it's only used in ratio
    let currentDistance = (touch1.x - touch2.x)**2   (touch1.y - touch2.y)**2
    
    if (initialPinchDistance == null)
    {
        initialPinchDistance = currentDistance
    }
    else
    {
        adjustZoom( null, currentDistance/initialPinchDistance )
    }
}

function adjustZoom(zoomAmount, zoomFactor)
{
    if (!isDragging)
    {
        if (zoomAmount)
        {
            cameraZoom  = zoomAmount
        }
        else if (zoomFactor)
        {
            console.log(zoomFactor)
            cameraZoom = zoomFactor*lastZoom
        }
        
        cameraZoom = Math.min( cameraZoom, MAX_ZOOM )
        cameraZoom = Math.max( cameraZoom, MIN_ZOOM )
        
        console.log(zoomAmount)
    }
}

canvas.addEventListener('mousedown', onPointerDown)
canvas.addEventListener('touchstart', (e) => handleTouch(e, onPointerDown))
canvas.addEventListener('mouseup', onPointerUp)
canvas.addEventListener('touchend',  (e) => handleTouch(e, onPointerUp))
canvas.addEventListener('mousemove', onPointerMove)
canvas.addEventListener('touchmove', (e) => handleTouch(e, onPointerMove))
canvas.addEventListener( 'wheel', (e) => adjustZoom(e.deltaY*SCROLL_SENSITIVITY))

// Ready, set, go
draw()
html, body
{
    height: 100%;
    margin: 0;
    padding: 0px;
    overflow: hidden;
}

#canvas
{
 /* width: 100%;
  height: 100%;*/
  background: #111;
}
<canvas id="canvas"></canvas>

  • Related