I am trying to rotate a square about its center in javascript. Although in this case I am using canvas to draw the square, I need to have a functional method of rotating the square for use elsewhere so just rotating the canvas is not acceptable. This is what I have so far
var thecanvas = this.myCanvas;
var canvaswidth = thecanvas.width;
tlx = (0 - ((canvaswidth * 0.6) / 2));
tly = (0 - ((canvaswidth * 0.6) / 2));
trx = (0 + ((canvaswidth * 0.6) / 2));
tryy = (0 - ((canvaswidth * 0.6) / 2));
blx = (0 - ((canvaswidth * 0.6) / 2));
bly = (0 + ((canvaswidth * 0.6) / 2));
brx = (0 + ((canvaswidth * 0.6) / 2));
bry = (0 + ((canvaswidth * 0.6) / 2));
tlx = (((tlx) * (this._cosD(orientation))) - ((tly) * (this._sinD(orientation))));
tly = ((tlx) * (this._sinD(orientation)) + (tly) * (this._cosD(orientation)));
trx = (((trx) * (this._cosD(orientation))) - ((tryy) * (this._sinD(orientation))));
tryy = ((trx) * (this._sinD(orientation)) + (tryy) * (this._cosD(orientation)));
blx = ((blx) * (this._cosD(orientation)) - (bly) * (this._sinD(orientation)));
bly = ((blx) * (this._sinD(orientation)) + (bly) * (this._cosD(orientation)));
brx = ((brx) * (this._cosD(orientation)) - (bry) * (this._sinD(orientation)));
bry = ((brx) * (this._sinD(orientation)) + (bry) * (this._cosD(orientation)));
tlx = (tlx + (canvaswidth / 2));
tly = (tly + (canvaswidth / 2));
trx = (trx + (canvaswidth / 2));
tryy = (tryy + (canvaswidth / 2));
blx = (blx + (canvaswidth / 2));
bly = (bly + (canvaswidth / 2));
brx = (brx + (canvaswidth / 2));
bry = (bry + (canvaswidth / 2));
var c2 = thecanvas.getContext('2d');
c2.fillStyle = '#f00';
c2.beginPath();
c2.moveTo(tlx, tly);
c2.lineTo(trx, tryy);
c2.lineTo(brx, bry);
c2.lineTo(blx, bly);
c2.closePath();
c2.fill();`
orientation is a value from -90-90 in degrees. This code starts to rotate the square but the square continues to "squish" until it is completely gone by 90 degrees. Obviously my formula for rotation is thrown off somewhere but I cant figure out how.
You could manually implement a transformation matrix. This allows you to set up the matrix for both read and write, then apply it to the points returning absolute points with the new actual values without the need or hassle to make special code for each use-case.
The formula for the (2D affine) using a 3x3 matrix would be:
or simplified in JavaScript:
var newX = x * a + y * c + e, // e (or tx) x 1 => e
newY = x * b + y * d + f; // f (or ty) x 1 => f
Now you would need to multiply the matrix with another to add rotation, translation, scale etc. The common way of doing this is this way - assume we have an object holding our values, you could do:
function Matrix() {
this.a = 1; // 1,0,0,1,0,0 = identity matrix (untransformed)
this.b = 0;
this.c = 0;
this.d = 1;
this.e = 0;
this.f = 0;
}
Matrix.prototype = {
// we need to be able to multiply matrices to accumulate values:
transform: function(a2, b2, c2, d2, e2, f2) {
var a1 = this.a,
b1 = this.b,
c1 = this.c,
d1 = this.d,
e1 = this.e,
f1 = this.f;
/* matrix order (canvas compatible):
* ace
* bdf
* 001
*/
this.a = a1 * a2 + c1 * b2;
this.b = b1 * a2 + d1 * b2;
this.c = a1 * c2 + c1 * d2;
this.d = b1 * c2 + d1 * d2;
this.e = a1 * e2 + c1 * f2 + e1;
this.f = b1 * e2 + d1 * f2 + f1;
}
}
With the core set up you can now add methods to do for example rotation:
rotate: function(angle) {
var cos = Math.cos(angle),
sin = Math.sin(angle);
this.transform(cos, sin, -sin, cos, 0, 0);
}
scale:
scale: function(sx, sy) {
this.transform(sx, 0, 0, sy, 0, 0);
}
translate:
translate: function(tx, ty) {
this.transform(1, 0, 0, 1, tx, ty);
}
and so forth depending on your needs. Note that these will accumulate as with the ordinary canvas/SVG matrix. Reset to identity to remove all transforms.
All you need to do now is to feed your points after settings transformation to get the absolute points - lets say we have a box of 100x100 we want to rotate at center:
Adding this to the prototype:
applyToPoint: function(p) {
return {
x: p.x * this.a + p.y * this.c + this.e,
y: p.x * this.b + p.y * this.d + this.f
}
}
will allow us to do:
var m = new Matrix();
m.translate(50, 50);
m.rotate(0.3);
m.translate(-50, 50);
var points = [
{x: 0, y: 0}, // upper-left
{x: 100, y: 0}, // upper-right
{x: 100, y: 100}, // bottom-right
{x: 0, y: 100} // bottom-left
],
result = [],
i = 0, p;
// transform points
while(p = points[i++]) result.push(m.applyToPoint(p));
Demo
Red box is original coordinates, blue is the transformed points which we now have access to:
function Matrix() {
this.a = 1; // identity matrix
this.b = 0;
this.c = 0;
this.d = 1;
this.e = 0;
this.f = 0;
}
Matrix.prototype = {
applyToPoint: function(p) {
return {
x: p.x * this.a + p.y * this.c + this.e,
y: p.x * this.b + p.y * this.d + this.f
}
},
transform: function(a2, b2, c2, d2, e2, f2) {
var a1 = this.a,
b1 = this.b,
c1 = this.c,
d1 = this.d,
e1 = this.e,
f1 = this.f;
/* matrix order (canvas compatible):
* ace
* bdf
* 001
*/
this.a = a1 * a2 + c1 * b2;
this.b = b1 * a2 + d1 * b2;
this.c = a1 * c2 + c1 * d2;
this.d = b1 * c2 + d1 * d2;
this.e = a1 * e2 + c1 * f2 + e1;
this.f = b1 * e2 + d1 * f2 + f1;
},
rotate: function(angle) {
var cos = Math.cos(angle),
sin = Math.sin(angle);
this.transform(cos, sin, -sin, cos, 0, 0);
},
scale: function(sx, sy) {
this.transform(sx, 0, 0, sy, 0, 0);
},
translate: function(tx, ty) {
this.transform(1, 0, 0, 1, tx, ty);
}
};
// apply some transformation:
var m = new Matrix(); // our manual transformation-matrix
m.translate(50, 50); // center of box
m.rotate(0.3); // some angle in radians
m.translate(-50, -50); // translate back
var points = [
{x: 0, y: 0}, // upper-left
{x: 100, y: 0}, // upper-right
{x: 100, y: 100}, // bottom-right
{x: 0, y: 100} // bottom-left
],
result = [], i = 0, p;
// transform points
while(p = points[i++]) result.push(m.applyToPoint(p));
// draw boxes to canvas:
var ctx = document.querySelector("canvas").getContext("2d");
ctx.translate(30, 30); // give some room for rotation for this demo
drawPolygon(points, "red");
drawPolygon(result, "blue");
drawCoord(points[0]); // plot old point
drawCoord(result[0]); // plot resulting point
// Compare using ordinary canvas: -------------------
ctx.translate(150, 0); // give some space
ctx.fillText("Regular canvas:", 0, -20);
drawPolygon(points, "red");
ctx.translate(50, 50); // center of box
ctx.rotate(0.3); // some angle in radians
ctx.translate(-50, -50); // translate back
drawPolygon(points, "blue");
// plot result:
function drawPolygon(pts, color) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.moveTo(pts[0].x, pts[0].y);
for(var i = 1, p; p = pts[i++];) ctx.lineTo(p.x, p.y);
ctx.closePath();
ctx.stroke();
}
function drawCoord(p) {
ctx.fillStyle = "#0c0"; ctx.fillRect(p.x - 2, p.y - 2, 4, 4);
ctx.fillStyle = "#000";
ctx.fillText(p.x.toFixed(1) + "," + p.y.toFixed(1), p.x, p.y - 2);
}
<canvas><canvas>
If you don't want to implement this yourselves, or want a more extensive solution, feel free to check out my (free) matrix solution here.
The future will give us currentTransform
on the context (currently only available in Chrome, Firefox struggles with a bug) returning a SVGMatrix
object which you can use in pretty much the same way as the above implementation.