【问题标题】:HTML5 canvas transform vs manual offsets?HTML5 画布转换与手动偏移?
【发布时间】:2017-04-24 22:53:52
【问题描述】:

关于画布性能的一个常见问题是对上下文状态的更改(如平移、缩放、旋转等)代价高昂,应保持在最低限度(例如,通过批处理使用相同的变换在一起)。

所以我的问题是,当您没有那么多从转换中受益的命令并且您不能真正批量处理它们时,使用手动偏移而不是转换会更好吗?还是进行适当的转换总是更好?

例如,如果我正在绘制每个图形可能包含 1-5 个多边形的小图形,并且每个图形都需要不同的变换(例如不同的放置和旋转),那么对每个图形进行完整变换似乎效率低下当我可以用一点三角函数计算出正确的位置时。

【问题讨论】:

  • 计算一个位置,然后渲染它总是比变换快,据我所知。但根据您需要进行的渲染量,差异可以忽略不计。
  • @Cerbrus 感谢您的回复,这正是我的想法!而且我可能会有很多这样的图形——可能多达数百个——所以在某些情况下,差异很可能对性能至关重要。
  • 翻译是否包括旋转?还是只是定位?
  • 这是放置和旋转,这使得偏移计算更加复杂,但应该仍然可行。
  • 我发现设置transform translate x,y,rotate r,uniform scale的最快方法如下xx=Math.cos(r)*scale;xy=Math.sin(r)*scale;ctx.setTransform(xx,xy,-xy,xx,x,y);这两个trig函数可能看起来很慢但是它们比ctx.rotate 调用更快。将它用于所有渲染调用,您无需恢复。

标签: javascript performance html5-canvas


【解决方案1】:

仅对于平移(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的矩阵
  • 您可以使用单独的 translate, rotate & scale 命令更改 Canvas 的矩阵
  • 你可以用context.setTransform完全覆盖Canvas的矩阵,
  • 但您无法读取 Canvas 的内部转换矩阵——它是只写的。

为什么要使用变换矩阵?

转换矩阵允许您将许多单独的平移、旋转和缩放聚合到一个易于重新应用的矩阵中。

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

一些示例用法:

  • 测试鼠标是否位于您已平移、旋转和缩放的形状内

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

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

  • 重绘一个经过广泛平移、旋转和缩放的形状。

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

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

    您可以使用几何学和三角学来计算构成变换形状的点,但使用变换矩阵来计算这些点会更快。

转换矩阵“类”

此代码反映了本机 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);
})();

演示:

此演示使用上面的转换矩阵“类”来:

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

  • 在不使用上下文转换命令的情况下重绘转换后的矩形。

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

代码:

<!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>

【讨论】:

    【解决方案2】:

    markE 的回答非常好,但这是我最终自己决定的:

    虽然 - 正如 K3N 在评论中指出的那样 - 所有绘制操作都经过变换矩阵,但这实际上不是问题。画布状态更改(相对)昂贵是 - 当然包括 setTransform。对每一件小事都进行 setTransform 调用效率低下,特别是如果它没有为您节省任何计算(您仍然必须进行三角计算才能将它们传递给 setTransform)。仅当您使用相同的变换进行大量绘图时,性能方面的变换才会带来好处。请记住,计算机非常擅长数学。

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

    我还想强调 Blindman67 关于如何在单个 setTransform 调用中高效地进行平移、旋转和缩放的评论:

    我发现设置转换翻译的最快方法 x,y,旋转r,统一scale如下 xx=Math.cos(r)*scale;xy=Math.sin(r)*scale;ctx.setTransform(x‌​x,xy,-xy,xx,x,y); 这两个三角函数可能看起来很慢,但它们比 ctx.rotate 打电话。将它用于所有渲染调用,您不需要 恢复。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多