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
,rotater
,uniformscale
is as followsxx=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 actx.rotate
call. Use it for all render calls and you don't need to restore. – Blindman67 Commented Dec 10, 2016 at 19:03
2 Answers
Reset to default 6For 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
,rotater
,uniformscale
is as followsxx=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 actx.rotate
call. Use it for all render calls and you don't need to restore.
本文标签: javascriptHTML5 canvas transform vs manual offsetsStack Overflow
版权声明:本文标题:javascript - HTML5 canvas transform vs manual offsets? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742279301a2445794.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论