admin管理员组

文章数量:1332404

One thing that is often said about canvas performance is that changes to the context's state (like translates, scales, rotates, etc...) are expensive and should be kept to a minimum (e.g. through batching draw mands that use the same transform together).

So my question is, is it better to use manual offsets over transforms when you don't have that many mands that benefit from the transform and you can't really batch them? Or is doing a proper transform just always better?

For example, if I'm drawing little graphics consisting of maybe 1-5 polygons per graphic, and each graphic needs a different transform (e.g. different placement and rotation), it seems inefficient to do a full transform for each of them when I could just calculate the proper positions with a bit of trigonometry.

One thing that is often said about canvas performance is that changes to the context's state (like translates, scales, rotates, etc...) are expensive and should be kept to a minimum (e.g. through batching draw mands that use the same transform together).

So my question is, is it better to use manual offsets over transforms when you don't have that many mands that benefit from the transform and you can't really batch them? Or is doing a proper transform just always better?

For example, if I'm drawing little graphics consisting of maybe 1-5 polygons per graphic, and each graphic needs a different transform (e.g. different placement and rotation), it seems inefficient to do a full transform for each of them when I could just calculate the proper positions with a bit of trigonometry.

Share Improve this question asked Dec 9, 2016 at 12:43 WingbladeWingblade 10.1k10 gold badges36 silver badges52 bronze badges 10
  • 1 Calculating a position, then rendering it is always faster than transform, as far as I know. But depending on the amount of rendering you need to do, the difference is negligible. – Cerbrus Commented Dec 9, 2016 at 12:46
  • @Cerbrus Thanks for the response, just what I thought! And I'll likely have quite a few of these graphics - potentially up to several hundred - so the difference may well be performance-critical in some scenarios. – Wingblade Commented Dec 9, 2016 at 12:52
  • Does the translation include rotation? Or is it just positioning? – Cerbrus Commented Dec 9, 2016 at 12:53
  • It's placement & rotation, which makes offset calculations a bit more involved but should still be doable. – Wingblade Commented Dec 9, 2016 at 12:55
  • 2 I have found that the quickest way to set transform translate x,y,rotate r,uniform scale is as follows xx=Math.cos(r)*scale;xy=Math.sin(r)*scale;ctx.setTransform(xx,xy,-xy,xx,x,y); The two trig functions may look slow but they are quicker than a ctx.rotate call. Use it for all render calls and you don't need to restore. – Blindman67 Commented Dec 10, 2016 at 19:03
 |  Show 5 more ments

2 Answers 2

Reset to default 6

For translations (x,y positioning) only, you might as well calculate the x,y yourself because you have to supply that when drawing anyway.

For rotations, scaling, etc. use individual transformations for individual polygons -- transformations, when needed, are not THAT expensive. And transformations are mostly done on the faster GPU anyway) ;-)

Note: use context.setTransform(1,0,0,1,0,0) to reset the individual transformations rather than context.save because context.restore will have the extra burden of saving / resetting all the non-transformational context states (styles, etc).

See below for an example of how to track individual transformations using the transformation matrix:



Canvas allows you to context.translate, context.rotate and context.scale in order to draw your shape in the position & size you require.

Canvas itself uses a transformation matrix to efficiently track transformations.

  • You can change Canvas's matrix with context.transform
  • You can change Canvas's matrix with individual translate, rotate & scale mands
  • You can pletely overwrite Canvas's matrix with context.setTransform,
  • But you can't read Canvas's internal transformation matrix -- it's write-only.

Why use a transformation matrix?

A transformation matrix allows you to aggregate many individual translations, rotations & scalings into a single, easily reapplied matrix.

During plex animations you might apply dozens (or hundreds) of transformations to a shape. By using a transformation matrix you can (almost) instantly reapply those dozens of transformations with a single line of code.

Some Example uses:

  • Test if the mouse is inside a shape that you have translated, rotated & scaled

    There is a built-in context.isPointInPath that tests if a point (eg the mouse) is inside a path-shape, but this built-in test is very slow pared to testing using a matrix.

    Efficiently testing if the mouse is inside a shape involves taking the mouse position reported by the browser and transforming it in the same way that the shape was transformed. Then you can apply hit-testing as if the shape was not transformed.

  • Redraw a shape that has been extensively translated, rotated & scaled.

    Instead of reapplying individual transformations with multiple .translate, .rotate, .scale you can apply all the aggregated transformations in a single line of code.

  • Collision test shapes that have been translated, rotated & scaled

    You can use geometry & trigonometry to calculate the points that make up transformed shapes, but it's faster to use a transformation matrix to calculate those points.

A Transformation Matrix "Class"

This code mirrors the native context.translate, context.rotate, context.scale transformation mands. Unlike the native canvas matrix, this matrix is readable and reusable.

Methods:

  • translate, rotate, scale mirror the context transformation mands and allow you to feed transformations into the matrix. The matrix efficiently holds the aggregated transformations.

  • setContextTransform takes a context and sets that context's matrix equal to this transformation matrix. This efficiently reapplies all transformations stored in this matrix to the context.

  • resetContextTransform resets the context's transformation to it's default state (==untransformed).

  • getTransformedPoint takes an untransformed coordinate point and converts it into a transformed point.

  • getScreenPoint takes a transformed coordinate point and converts it into an untransformed point.

  • getMatrix returns the aggregated transformations in the form of a matrix array.

Code:

var TransformationMatrix=( function(){
    // private
    var self;
    var m=[1,0,0,1,0,0];
    var reset=function(){ var m=[1,0,0,1,0,0]; }
    var multiply=function(mat){
        var m0=m[0]*mat[0]+m[2]*mat[1];
        var m1=m[1]*mat[0]+m[3]*mat[1];
        var m2=m[0]*mat[2]+m[2]*mat[3];
        var m3=m[1]*mat[2]+m[3]*mat[3];
        var m4=m[0]*mat[4]+m[2]*mat[5]+m[4];
        var m5=m[1]*mat[4]+m[3]*mat[5]+m[5];
        m=[m0,m1,m2,m3,m4,m5];
    }
    var screenPoint=function(transformedX,transformedY){
        // invert
        var d =1/(m[0]*m[3]-m[1]*m[2]);
        im=[ m[3]*d, -m[1]*d, -m[2]*d, m[0]*d, d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5]) ];
        // point
        return({
            x:transformedX*im[0]+transformedY*im[2]+im[4],
            y:transformedX*im[1]+transformedY*im[3]+im[5]
        });
    }
    var transformedPoint=function(screenX,screenY){
        return({
            x:screenX*m[0] + screenY*m[2] + m[4],
            y:screenX*m[1] + screenY*m[3] + m[5]
        });    
    }
    // public
    function TransformationMatrix(){
        self=this;
    }
    // shared methods
    TransformationMatrix.prototype.translate=function(x,y){
        var mat=[ 1, 0, 0, 1, x, y ];
        multiply(mat);
    };
    TransformationMatrix.prototype.rotate=function(rAngle){
        var c = Math.cos(rAngle);
        var s = Math.sin(rAngle);
        var mat=[ c, s, -s, c, 0, 0 ];    
        multiply(mat);
    };
    TransformationMatrix.prototype.scale=function(x,y){
        var mat=[ x, 0, 0, y, 0, 0 ];        
        multiply(mat);
    };
    TransformationMatrix.prototype.skew=function(radianX,radianY){
        var mat=[ 1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0 ];
        multiply(mat);
    };
    TransformationMatrix.prototype.reset=function(){
        reset();
    }
    TransformationMatrix.prototype.setContextTransform=function(ctx){
        ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
    }
    TransformationMatrix.prototype.resetContextTransform=function(ctx){
        ctx.setTransform(1,0,0,1,0,0);
    }
    TransformationMatrix.prototype.getTransformedPoint=function(screenX,screenY){
        return(transformedPoint(screenX,screenY));
    }
    TransformationMatrix.prototype.getScreenPoint=function(transformedX,transformedY){
        return(screenPoint(transformedX,transformedY));
    }
    TransformationMatrix.prototype.getMatrix=function(){
        var clone=[m[0],m[1],m[2],m[3],m[4],m[5]];
        return(clone);
    }
    // return public
    return(TransformationMatrix);
})();

Demo:

This demo uses the Transformation Matrix "Class" above to:

  • Track (==save) a rectangle's transformation matrix.

  • Redraw the transformed rectangle without using context transformation mands.

  • Test if the mouse has clicked inside the transformed rectangle.

window.onload=(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var cw=canvas.width;
    var ch=canvas.height;
    function reOffset(){
        var BB=canvas.getBoundingClientRect();
        offsetX=BB.left;
        offsetY=BB.top;        
    }
    var offsetX,offsetY;
    reOffset();
    window.onscroll=function(e){ reOffset(); }
    window.onresize=function(e){ reOffset(); }

    // Transformation Matrix "Class"

    var TransformationMatrix=( function(){
        // private
        var self;
        var m=[1,0,0,1,0,0];
        var reset=function(){ var m=[1,0,0,1,0,0]; }
        var multiply=function(mat){
            var m0=m[0]*mat[0]+m[2]*mat[1];
            var m1=m[1]*mat[0]+m[3]*mat[1];
            var m2=m[0]*mat[2]+m[2]*mat[3];
            var m3=m[1]*mat[2]+m[3]*mat[3];
            var m4=m[0]*mat[4]+m[2]*mat[5]+m[4];
            var m5=m[1]*mat[4]+m[3]*mat[5]+m[5];
            m=[m0,m1,m2,m3,m4,m5];
        }
        var screenPoint=function(transformedX,transformedY){
            // invert
            var d =1/(m[0]*m[3]-m[1]*m[2]);
            im=[ m[3]*d, -m[1]*d, -m[2]*d, m[0]*d, d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5]) ];
            // point
            return({
                x:transformedX*im[0]+transformedY*im[2]+im[4],
                y:transformedX*im[1]+transformedY*im[3]+im[5]
            });
        }
        var transformedPoint=function(screenX,screenY){
            return({
                x:screenX*m[0] + screenY*m[2] + m[4],
                y:screenX*m[1] + screenY*m[3] + m[5]
            });    
        }
        // public
        function TransformationMatrix(){
            self=this;
        }
        // shared methods
        TransformationMatrix.prototype.translate=function(x,y){
            var mat=[ 1, 0, 0, 1, x, y ];
            multiply(mat);
        };
        TransformationMatrix.prototype.rotate=function(rAngle){
            var c = Math.cos(rAngle);
            var s = Math.sin(rAngle);
            var mat=[ c, s, -s, c, 0, 0 ];    
            multiply(mat);
        };
        TransformationMatrix.prototype.scale=function(x,y){
            var mat=[ x, 0, 0, y, 0, 0 ];        
            multiply(mat);
        };
        TransformationMatrix.prototype.skew=function(radianX,radianY){
            var mat=[ 1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0 ];
            multiply(mat);
        };
        TransformationMatrix.prototype.reset=function(){
            reset();
        }
        TransformationMatrix.prototype.setContextTransform=function(ctx){
            ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
        }
        TransformationMatrix.prototype.resetContextTransform=function(ctx){
            ctx.setTransform(1,0,0,1,0,0);
        }
        TransformationMatrix.prototype.getTransformedPoint=function(screenX,screenY){
            return(transformedPoint(screenX,screenY));
        }
        TransformationMatrix.prototype.getScreenPoint=function(transformedX,transformedY){
            return(screenPoint(transformedX,transformedY));
        }
        TransformationMatrix.prototype.getMatrix=function(){
            var clone=[m[0],m[1],m[2],m[3],m[4],m[5]];
            return(clone);
        }
        // return public
        return(TransformationMatrix);
    })();

    // DEMO starts here

    // create a rect and add a transformation matrix
    // to track it's translations, rotations & scalings
    var rect={x:30,y:30,w:50,h:35,matrix:new TransformationMatrix()};

    // draw the untransformed rect in black
    ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
    // Demo: label
    ctx.font='11px arial';
    ctx.fillText('Untransformed Rect',rect.x,rect.y-10);

    // transform the canvas & draw the transformed rect in red
    ctx.translate(100,0);
    ctx.scale(2,2);
    ctx.rotate(Math.PI/12);
    // draw the transformed rect
    ctx.strokeStyle='red';
    ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
    ctx.font='6px arial';
    // Demo: label
    ctx.fillText('Same Rect: Translated, rotated & scaled',rect.x,rect.y-6);
    // reset the context to untransformed state
    ctx.setTransform(1,0,0,1,0,0);

    // record the transformations in the matrix
    var m=rect.matrix;
    m.translate(100,0);
    m.scale(2,2);
    m.rotate(Math.PI/12);

    // use the rect's saved transformation matrix to reposition, 
    //     resize & redraw the rect
    ctx.strokeStyle='blue';
    drawTransformedRect(rect);

    // Demo: instructions
    ctx.font='14px arial';
    ctx.fillText('Demo: click inside the blue rect',30,200);

    // redraw a rect based on it's saved transformation matrix
    function drawTransformedRect(r){
        // set the context transformation matrix using the rect's saved matrix
        m.setContextTransform(ctx);
        // draw the rect (no position or size changes needed!)
        ctx.strokeRect( r.x, r.y, r.w, r.h );
        // reset the context transformation to default (==untransformed);
        m.resetContextTransform(ctx);
    }

    // is the point in the transformed rectangle?
    function isPointInTransformedRect(r,transformedX,transformedY){
        var p=r.matrix.getScreenPoint(transformedX,transformedY);
        var x=p.x;
        var y=p.y;
        return(x>r.x && x<r.x+r.w && y>r.y && y<r.y+r.h);
    } 

    // listen for mousedown events
    canvas.onmousedown=handleMouseDown;
    function handleMouseDown(e){
        // tell the browser we're handling this event
        e.preventDefault();
        e.stopPropagation();
        // get mouse position
        mouseX=parseInt(e.clientX-offsetX);
        mouseY=parseInt(e.clientY-offsetY);
        // is the mouse inside the transformed rect?
        if(isPointInTransformedRect(rect,mouseX,mouseY)){
            alert('You clicked in the transformed Rect');
        }
    }

    // Demo: redraw transformed rect without using
    //       context transformation mands
    function drawTransformedRect(r,color){
        var m=r.matrix;
        var tl=m.getTransformedPoint(r.x,r.y);
        var tr=m.getTransformedPoint(r.x+r.w,r.y);
        var br=m.getTransformedPoint(r.x+r.w,r.y+r.h);
        var bl=m.getTransformedPoint(r.x,r.y+r.h);
        ctx.beginPath();
        ctx.moveTo(tl.x,tl.y);
        ctx.lineTo(tr.x,tr.y);
        ctx.lineTo(br.x,br.y);
        ctx.lineTo(bl.x,bl.y);
        ctx.closePath();
        ctx.strokeStyle=color;
        ctx.stroke();
    }

}); // end window.onload
body {
  background-color:white;
}
#canvas{
  border:1px solid red;
}
<canvas id="canvas" width="512" height="250"></canvas>

markE's answer is quite good, but here's what I ultimately ended up settling on for myself:

While - as pointed out by K3N in a ment - all draw operations go through the transform matrix, that's not actually the issue. Canvas state changes being (relatively) expensive is - that of course includes setTransform. Making a setTransform call for every little thing is inefficient especially if it isn't saving you any calculations (you still have to do trigonometry calculations in order to pass them to setTransform). Performance-wise transforms only provide an benefit if you're doing a lot of drawing with the same transform. Keep in mind that puters are very good at maths.

With that said, the performance difference is small enough that in the end it's best to go with what is easiest to work with as a programmer/provides the best abstraction. For example one might have some graphics in the form of functions that draw relative to the canvas origin point, so doing setTransform before each of them would allow positioning the graphics without the functions themselves needing to include logic for rotation/positioning/etc. i.e. using transforms would help encapsulation.

I'd also like to highlight Blindman67's ment on how to efficiently do translate, rotate, and scale in a single setTransform call:

I have found that the quickest way to set transform translate x,y,rotate r,uniform scale is as follows xx=Math.cos(r)*scale;xy=Math.sin(r)*scale;ctx.setTransform(x‌​x,xy,-xy,xx,x,y); The two trig functions may look slow but they are quicker than a ctx.rotate call. Use it for all render calls and you don't need to restore.

本文标签: javascriptHTML5 canvas transform vs manual offsetsStack Overflow