17

I want to zoom and pan an HTML5 Canvas by transforming the context using translate() and scale(), clearing the canvas, and then redrawing. Note that I am explicitly not calling save() and restore() around my transformations.

If I perform the standard ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height) then the entire visible canvas will not be cleared; downscaling or panning may cause this initial rectangle to not exactly cover the drawing area.

If I perform the Webkit-friendly clearing method...

var w=canvas.width;
canvas.width = 0;
canvas.width = w;

...then the cumulative transformation of the context is reset.

How can I best clear the entire canvas context without losing my transformation?

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • The sucker-ish way would be to store you scales and translates and then calculate the bounding rectangle yourself, but one would hope that there is a better way. – Jakub Hampl Apr 02 '11 at 23:10
  • @JakubHampl That's actually what I'm doing, since I'm already keeping track of the transform and have added method to transform from screen space to canvas space, but this seemed like a good opportunity to point out the problems with these two clearing methods and see if there's something cleaner. – Phrogz Apr 02 '11 at 23:24

2 Answers2

36

Keeping track of all the transformation information like you are presumably doing is what several others so far have done (like cake.js and my own library, for two). I think doing this will pretty much be an inevitability for any large canvas library.

Ilmari of cake.js even complained to mozilla: https://bugzilla.mozilla.org/show_bug.cgi?id=408804

You could instead call save/restore around your clear method:

// I have lots of transforms right now
ctx.save();
ctx.setTransform(1,0,0,1,0,0);
// Will always clear the right space
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.restore();
// Still have my old transforms

Won't that satisfy your case?

Simon Sarris
  • 62,212
  • 13
  • 141
  • 171
  • 1
    How silly of me; I was trying to think how to get the identity matrix, but was thinking of needing the current transform to calculate the inverse and then multiply by that. Yes, this should do nicely. – Phrogz Apr 03 '11 at 04:04
  • 1
    Glad I could help. I wanted to also say thanks for all your contributions to the canvas topic on S.O. – Simon Sarris Apr 04 '11 at 00:46
5

For those who care to track their full context transforms, here's my code for doing so in an on-demand, per-context basis. This is prefaced with the usage that shows how to clear the full rectangle based on the transformed coordinates. You can see the code in use on my website.

window.onload = function(){
  var canvas = document.getElementsByTagName('canvas')[0];
  var ctx = canvas.getContext('2d');
  trackTransforms(ctx);
  function redraw(){
    var p1 = ctx.transformedPoint(0,0);
    var p2 = ctx.transformedPoint(canvas.width,canvas.height);
    ctx.clearRect(p1.x,p1.y,p2.x-p1.x,p2.y-p1.y);
    // ... 
  }
}


// Adds ctx.getTransform(), returning an SVGMatrix
// Adds ctx.transformedPoint(x,y), returning an SVGPoint
function trackTransforms(ctx){
  var svg = document.createElementNS("http://www.w3.org/2000/svg",'svg');
  var xform = svg.createSVGMatrix();
  ctx.getTransform = function(){ return xform; };

  var savedTransforms = [];
  var save = ctx.save;
  ctx.save = function(){
    savedTransforms.push(xform.translate(0,0));
    return save.call(ctx);
  };
  var restore = ctx.restore;
  ctx.restore = function(){
    xform = savedTransforms.pop();
    return restore.call(ctx);
  };

  var scale = ctx.scale;
  ctx.scale = function(sx,sy){
    xform = xform.scaleNonUniform(sx,sy);
    return scale.call(ctx,sx,sy);
  };
  var rotate = ctx.rotate;
  ctx.rotate = function(radians){
    xform = xform.rotate(radians*180/Math.PI);
    return rotate.call(ctx,radians);
  };
  var translate = ctx.translate;
  ctx.translate = function(dx,dy){
    xform = xform.translate(dx,dy);
    return translate.call(ctx,dx,dy);
  };
  var transform = ctx.transform;
  ctx.transform = function(a,b,c,d,e,f){
    var m2 = svg.createSVGMatrix();
    m2.a=a; m2.b=b; m2.c=c; m2.d=d; m2.e=e; m2.f=f;
    xform = xform.multiply(m2);
    return transform.call(ctx,a,b,c,d,e,f);
  };
  var setTransform = ctx.setTransform;
  ctx.setTransform = function(a,b,c,d,e,f){
    xform.a = a;
    xform.b = b;
    xform.c = c;
    xform.d = d;
    xform.e = e;
    xform.f = f;
    return setTransform.call(ctx,a,b,c,d,e,f);
  };
  var pt  = svg.createSVGPoint();
  ctx.transformedPoint = function(x,y){
    pt.x=x; pt.y=y;
    return pt.matrixTransform(xform.inverse());
  }
}
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • This example ist great and helped me alot, but there is still one problem I can not get my head around: How would you clear the canvas in redraw(), if your image is rotated at serveral points of the image? – Kevkong Dec 06 '12 at 15:27
  • @Kevkong Exactly like the accepted answer shows: save the context transform, reset the transform to the identity matrix, clear the 0,0,width,height, and restore the transform. – Phrogz Dec 07 '12 at 17:36
  • Yea you are right, I just misspelled something so "restore" wouldnt work as intended and transformedPoint returned the wrong point after a restore. thanks so far. – Kevkong Dec 13 '12 at 17:12