Reputation: 1818
As you can see in the demo the L shape is getting cropped off the top of the screen and should be rotated 180 degrees and flush with the top left corner. I noticed two things that don't work as expected, the first is when I change ctx.translate(x, y)
to ctx.moveTo(x, y)
and increase the shape position to 100, 100
it moves more than 100px with translate, where as moveTo seems accurate. The second is that using a negative translate after ctx.stroke()
has no affect on the shapes position.
var shape = {};
function draw(shape) {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
var x = shape.position.x + 0.5;
var y = shape.position.y + 0.5;
ctx.translate(x, y);
ctx.translate(shape.width * shape.scale/2, shape.height * shape.scale/2);
ctx.rotate(shape.orientation * Math.PI/180);
ctx.beginPath();
for (var i = 0; i < shape.points.length; i++) {
x = shape.points[i].x * shape.scale + shape.position.x + 0.5;
y = shape.points[i].y * shape.scale + shape.position.y + 0.5;
ctx.lineTo(x, y);
}
ctx.strokeStyle = '#fff';
ctx.stroke();
ctx.translate(-shape.width * shape.scale/2, -shape.height * shape.scale/2);
ctx.restore();
}
}
// L Shape
shape.points = [];
shape.points.push({ x:0, y:0 });
shape.points.push({ x:0, y:3 });
shape.points.push({ x:2, y:3 });
shape.points.push({ x:2, y:2 });
shape.points.push({ x:1, y:2 });
shape.points.push({ x:1, y:0 });
shape.points.push({ x:0, y:0 });
shape.position = {x: 0, y: 0};
shape.scale = 30;
shape.width = 3;
shape.height = 2;
shape.orientation = 180;
draw(shape);
#canvas {
background: #272B34; }
<canvas id="canvas" width="400" height="600"></canvas>
Upvotes: 1
Views: 1114
Reputation: 54069
The easiest way to do 2D tranforms is via the setTransform function which takes 6 numbers, 2 vectors representing the direction and scale of the X and y axis, and one coordinate representing the new origin.
Unlike the other transform functions which are dependent of the current state setTransform is not effected by any transform done before it is called.
To set the transform for a matrix that has a square aspect (x and y scale are the same) and that the y axis is at 90 deg to the x ( no skewing) and a rotation is as follows
// x,y the position of the orign
function setMatrix(x,y,scale,rotate){
var xAx = Math.cos(rotate) * scale; // the x axis x
var xAy = Math.sin(rotate) * scale; // the x axis y
ctx.setTransform(xAx, xAy, -xAy, xAx, x, y);
}
//use
setMatrix(100,100,20,Math.PI / 4);
ctx.strokeRect(-2,-2,4,4); // draw a square centered at 100,100
// scaled 20 times
// and rotate clockwise 45 deg
In response to the questions in the comments.
Can you also explain why you used cos and sin for the axis?
I use Math.sin
and Math.cos
to calculate the X axis and thus the Y axis as well (because y is at 90 deg to x) because it is slightly quicker than adding the rotation as a separate transform.
When you use any of the transform functions apart from setTransform
you are doing a matrix multiplication. The next snippet is the JS equivalent minimum calculations done when using ctx.rotate
, ctx.scale
, ctx.translate
, or ctx.transform
// mA represent the 2D context current transform
mA = [1,0,0,1,0,0]; // default transform
// mB represents the transform to apply
mB = [0,1,-1,0,0,0]; // Rotate 90 degree clockwise
// m is the resulting matrix
m[0] = mA[0] * mB[0] + mA[2] * mB[1];
m[1] = mA[1] * mB[0] + mA[3] * mB[1];
m[2] = mA[0] * mB[2] + mA[2] * mB[3];
m[3] = mA[1] * mB[2] + mA[3] * mB[3];
m[4] = mA[0] * mB[0] + mA[2] * mB[1] + mA[4];
m[5] = mA[1] * mB[0] + mA[3] * mB[1] + mA[5];
As you can see there are 12 multiplications and 6 additions plus the need for memory to hold the intermediate values and if the call was to ctx.rotation
the sin and cos of the angle would also be done. This is all done in native code in the JavaScript engine so is quicker than doing in JS, but side stepping the matrix multiplication by calculating the axis in JavaScript results in less work. Using setTransform
simply replaces the current matrix and does not require a matrix multiplication to be performed.
The alternative to the answer's setMatrix
function can be
function setMatrix(x,y,scale,rotate){
ctx.setTransform(scale,0,0,scale, x, y); // set current matrix
ctx.rotate(rotate); // multiply current matrix with rotation matrix
}
which does the same and does look cleaner, though is slower and when you want to do things like games where performance is very important often called functions should be as quick as possible.
setMatrix
functionSo how would I use this for custom shapes like the L in my demo?
Replacing your draw function. BTW you should be getting the context outside any draw function.
// assumes ctx is the 2D context in scope for this function.
function draw(shape) {
var i = 0;
setMatrix(shape.position.x, shape.position.y, shape.scale, shape.orientation); // orientation is in radians
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.moveTo(shape.points[i].x, shape.points[i++].y)
while (i < shape.points.length) {
ctx.lineTo(shape.points[i].x, shape.points[i++].y);
}
ctx.closePath(); // draw line from end to start
ctx.stroke();
}
In your code you have the line stored such that its origin (0,0) is at the top left. When defining shapes you should define it in terms of its local coordinates. This will define the point of rotation and scaling and represents the coordinate that will be at the transforms origin (position x,y).
Thus you should define your shape at its origin
function createShape(originX, originY, points){
var i;
const shape = [];
for(i = 0; i < points.length; i++){
shape.push({
x : points[i][0] - originX,
y : points[i][1] - originY,
});
}
return shape;
}
const shape = {};
shape.points = createShape(
1,1.5, // the local origin relative to the coords on next line
[[0,0],[0,3],[2,3],[2,2],[1,2],[1,0]] // shape coords
);
Upvotes: 5