HTML5 canvas transform vs manual offsets?

时间:2016-12-09 12:43:06

标签: javascript performance html5-canvas

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 commands 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 commands 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.

2 个答案:

答案 0 :(得分:2)

仅对于翻译(x,y定位),您也可以自己计算x,y,因为无论如何都必须提供。

对于旋转,缩放等,对单个多边形使用单独的变换 - 在需要时进行变换并不昂贵。转换主要是在更快的GPU上完成的;) - )

注意:使用context.setTransform(1,0,0,1,0,0)重置单个转换而不是context.save,因为context.restore将有额外的负担来保存/重置所有非转换上下文状态(样式等)

请参阅下文,了解如何使用转换矩阵跟踪单个转换的示例:

Canvas允许您context.translatecontext.rotatecontext.scale,以便在位置和位置绘制您的形状。你需要的尺寸。

Canvas本身使用变换矩阵来有效地跟踪变换。

  • 您可以使用context.transform
  • 更改Canvas&#39的矩阵
  • 您可以使用单独的translate, rotate & scale命令
  • 更改Canvas的矩阵
  • 您可以使用context.setTransform
  • 完全覆盖Canvas的矩阵
  • 但您无法阅读Canvas的内部转换矩阵 - 它是只写的。

为什么要使用转换矩阵?

转换矩阵允许您汇总许多单独的翻译,旋转和放大。缩放成单个,易于重新应用的矩阵。

在复杂动画期间,您可以对形状应用数十(或数百)个变换。通过使用转换矩阵,您可以(几乎)立即使用一行代码重新应用这些转换。

一些示例使用:

  • 测试鼠标是否在您已翻译,旋转和放大的形状内。缩放

    有一个内置的context.isPointInPath可以测试一个点(例如鼠标)是否在路径形状内,但与使用矩阵测试相比,这个内置测试非常慢。

    有效测试鼠标是否在形状内部涉及获取浏览器报告的鼠标位置并以与变形形状相同的方式对其进行转换。然后,您可以应用命中测试,就好像形状没有被转换一样。

  • 重绘已广泛翻译,旋转和放大的形状缩放。

    您可以在一行代码中应用所有聚合转换,而不是使用多个.translate, .rotate, .scale重新应用单个转换。

  • 已经平移,旋转和放大的碰撞测试形状缩放

    你可以使用几何&三角法计算构成变换形状的点,但使用变换矩阵计算这些点的速度更快。

转换矩阵" Class"

此代码镜像原生context.translatecontext.rotatecontext.scale转换命令。与原生画布矩阵不同,此矩阵是可读和可重用的。

方法:

  • translaterotatescale镜像了上下文转换命令,并允许您将转换提供给矩阵。矩阵有效地保持了聚合变换。

  • setContextTransform获取上下文并将该上下文矩阵设置为等于此变换矩阵。这有效地将存储在该矩阵中的所有变换应用于上下文。

  • resetContextTransform重置上下文转换为默认状态(==未转换)。

  • getTransformedPoint采用未转换的坐标点并将其转换为转换点。

  • getScreenPoint采用变换后的坐标点并将其转换为未转换的点。

  • getMatrix以矩阵数组的形式返回聚合转换。

<强>代码:

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);
})();

演示:

此演示使用转换矩阵&#34; Class&#34;以上:

  • 跟踪(==保存)矩形的变换矩阵。

  • 不使用上下文转换命令重绘已转换的矩形。

  • 测试鼠标是否已在变换后的矩形内单击。

代码:

<!doctype html>
<html>
<head>
<style>
    body{ background-color:white; }
    #canvas{border:1px solid red; }
</style>
<script>
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 commands
    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
</script>
</head>
<body>
    <canvas id="canvas" width=512 height=250></canvas>
</body>
</html>

答案 1 :(得分:0)

markE的回答非常好,但这就是我最终为自己解决的问题:

虽然 - 正如K3N在评论中指出的那样 - 所有的绘制操作都经过变换矩阵,这实际上不是问题。画布状态变化(相对)昂贵 - 当然包括setTransform。对每个小东西进行setTransform调用都是低效的,特别是如果它没有为你保存任何计算(你仍然需要进行三角计算才能将它们传递给setTransform)。如果您使用相同的转换进行很多绘图,那么性能方面的转换只会提供一个好处。请记住,数学计算机非常好

话虽如此,性能差异足够小,最终最好与程序员一起使用最简单的东西/提供最好的抽象。例如,可能有一些相对于画布原点绘制的函数形式的图形,因此在每个图形之前执行setTransform将允许定位图形,而函数本身不需要包含旋转/定位/等的逻辑。即使用变换将有助于封装。

我还要强调Blindman67关于如何在单个setTransform调用中有效地进行翻译,旋转和缩放的评论:

  

我发现设置变换的最快方式是翻译   xy,轮换r,制服scale如下   xx=Math.cos(r)*scale;xy=Math.sin(r)*scale;ctx.setTransform(x‌​x,xy,-xy,xx,x,y);   两个trig函数可能看起来很慢,但它们比a快   ctx.rotate致电。将它用于所有渲染调用,您不需要   还原