Reputation: 559
Im trying to use the translate function when drawing a circle, but when i try to do it it doesnt behave properly. Instead of drawing the circle it draws this:
if the image doesnt show up: click here
This is my code for the drawing of the circle (inside a circle class):
ctx.strokeStyle = "white"
ctx.translate(this.x, this.y)
ctx.beginPath()
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
// tried with and without translating back, inside and outside of this function
ctx.translate(0, 0)
This is the rest of my code:
let canvas
let ctx
let circle
function init() {
canvas = document.querySelector("#canvas")
ctx = canvas.getContext("2d")
// x, y, radius
circle = new Circle(canvas.width/5, canvas.height/2, 175)
requestAnimationFrame(loop)
}
function loop() {
// Background
ctx.fillStyle = "black"
ctx.fillRect(0, 0, canvas.width, canvas.height)
// The function with the drawing of the circle
circle.draw()
requestAnimationFrame(loop)
}
Btw: When i dont use the translate function it draws the circle normally.
I answered my own question below as i found that the translate functions a little bit differently in javascript than how i thought it would.
Upvotes: 0
Views: 1172
Reputation: 54128
Your function
ctx.strokeStyle = "white"
ctx.translate(this.x, this.y)
ctx.beginPath()
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
// tried with and without translating back, inside and outside of this function
ctx.translate(0, 0)
Can be improved as follows
ctx.strokeStyle = "white"
ctx.setTransform(1, 0, 0, 1, this.x, this.y); //BM67 This call is faster than ctx.translate
ctx.beginPath()
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
// ctx.closePath() //BM67 This line does nothing and is not related to beginPath.
// tried with and without translating back, inside and outside of this function
//ctx.translate(0, 0) //BM67 You don't need to reset the transform
// The call to ctx.setTransfrom replaces
// the current transform before you draw the circle
and would look like
ctx.strokeStyle = "white"
ctx.setTransform(1, 0, 0, 1, this.x, this.y);
ctx.beginPath()
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
Why this is better will need you to understand how 2D transformations work and why some 2D API calls should not be used, and that 99% of all transformation needs can be done faster and with less mind f with ctx.setTransform
than the poorly named ctx.translate
, ctx.scale
, or ctx.rotate
Read on if interested.
When you render to the canvas all coordinates are transformed via the transformation matrix.
The matrix consists of 6 values as set by setTransform(a,b,c,d,e,f)
. The values a,b,c,d,e,f
are rather obscure and the literature does not help explaining them.
The best way to think of them is by what they do. I will rename them as setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY)
they represent the direction and size of the x axis, y axis and the origin.
xAxisX
, xAxisY
are X Axis X, X Axis YyAxisX
, yAxisY
are Y Axis X, Y Axis YoriginX
, originY
are the canvas real pixel coordinates of the originThe default transform is setTransform(1, 0, 0, 1, 0, 0)
meaning that the X Axis moves across 1 down 0, the Y Axis moves across 0 and down 1 and the origin is at 0, 0
You can manually apply the transform to a 2D point as follows
function transformPoint(x, y) {
return {
// Move x dist along X part of X Axis
// Move y dist along X part of Y Axis
// Move to the X origin
x : x * xAxisX + y * yAxisX + originX,
// Move x dist along Y part of X Axis
// Move y dist along Y part of Y Axis
// Move to the Y origin
y : x * xAxisY + y * yAxisY + originY,
};
}
If we substitute the default matrix setTransform(1, 0, 0, 1, 0, 0)
we get
{
x : x * 1 + y * 0 + 0,
y : x * 0 + y * 1 + 0,
}
// 0 * n is 0 so removing the * 0
{
x : x * 1,
y : y * 1,
}
// 1 time n is n so remove the * 1
{
x : x,
y : y,
}
As you can see the default transform does nothing to the point
If we set the translation ox, oy to setTransform(1, 0, 0, 1, 100, 200)
the transform is
{
x : x * 1 + y * 0 + 100,
y : x * 0 + y * 1 + 200,
}
// or simplified as
{
x : x + 100,
y : y + 200,
}
If we set the scale of the X Axis and Y Axis to setTransform(2, 0, 0, 2, 100, 200)
the transform is
{
x : x * 2 + y * 0 + 100,
y : x * 0 + y * 2 + 200,
}
// or simplified as
{
x : x * 2 + 100,
y : y * 2 + 200,
}
Rotation is a little more complex and requires some trig. You can use cos and sin to get a unit vector in a direction angle (NOTE all angles are in radians PI * 2
is 360deg, PI
is 180deg, PI / 2
is 90deg)
Thus the unit vector for 0 radians is
xAxisX = Math.cos(0);
yAxisY = Math.sin(0);
So for angles 0
, PI * (1 / 2)
, PI
, PI * (3 / 2)
, PI * 2
angle = 0;
xAxisX = Math.cos(angle); // 1
yAxisY = Math.sin(angle); // 0
angle = Math.PI * (1 / 2); // 90deg (points down screen)
xAxisX = Math.cos(angle); // 0
yAxisY = Math.sin(angle); // 1
angle = Math.PI; // 180deg (points to left screen)
xAxisX = Math.cos(angle); // -1
yAxisY = Math.sin(angle); // 0
angle = Math.PI * (3 / 2); // 270deg (points to up screen)
xAxisX = Math.cos(angle); // 0
yAxisY = Math.sin(angle); // -1
In 90% of cases when you transform points you want the points to remain square, that is the Y axis remains at PI / 2
(90deg) clockwise of the X axis and the Scale of the Y axis is the same as the scale of the X axis.
You can rotate a vector 90 deg by swapping the x and y and negating the new x
x = 1; // X axis points from left to right
y = 0; // No downward part
// Rotate 90deg clockwise
x90 = -y; // 0 no horizontal part
y90 = x; // Points down the screen
We can take advantage of this simple 90 rotation to create a uniform rotation by only defining the angle of the X Axis
xAxisX = Math.cos(angle);
xAxisY = Math.sin(angle);
// create a matrix as setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, 0, 0)
// to transform the point
{
x : x * xAxisX + y * (-xAxisY) + 0,
y : x * xAxisY + y * xAxisX + 0,
}
// to simplify
{
x : x * xAxisX - y * xAxisY,
y : x * xAxisY + y * xAxisX,
}
Using the above info you can now manually create a uniform matrix using only 4 values, The origin x
,y
the scale
, and the rotate
function transformPoint(x, y, originX, originY, scale, rotate) {
// get the direction of the X Axis
var xAxisX = Math.cos(rotate);
var xAxisY = Math.sin(rotate);
// Scale the x Axis
xAxisX *= Math.cos(rotate);
xAxisY *= Math.sin(rotate);
// Get the Y Axis as X Axis rotated 90 deg
const yAxisX = -xAxisY;
const yAxisY = xAxisX;
// we have the 6 values for the transform
// [xAxisX, xAxisY, yAxisX, yAxisY, originX, originY]
// Transform the point
return {
x : x * xAxisX + y * yAxisX + originX,
y : x * xAxisY + y * yAxisY + originY,
}
}
// we can simplify the above down to
function transformPoint(x, y, originX, originY, scale, rotate) {
// get the direction and scale of the X Axis
const xAxisX = Math.cos(rotate) * scale;
const xAxisY = Math.sin(rotate) * scale;
// Transform the point
return {
x : x * xAxisX - y * xAxisY + originX,
// note the ^ negative
y : x * xAxisY + y * xAxisX + originY,
}
}
Or we can create the matrix using ctx.setTransform
using the above and let the GPU hardware do the transform
function createTransform(originX, originY, scale, rotate) {
const xAxisX = Math.cos(rotate) * scale;
const xAxisY = Math.sin(rotate) * scale;
ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, originX, originY);
}
I will rename this section to
ctx.translate
, ctx.scale
, or ctx.rotate
The 2D API has some bad naming which is the reason for 90% of the transform question that appear in html5-canvas tag.
If we rename the API calls you will get a better understanding of what they do
ctx.translate(x, y); // should be ctx.multiplyCurrentMatirxWithTranslateMatrix
// or shorten ctx.matrixMutliplyTranslate(x, y)
The function ctx.translate
does not actually translate a point, but rather it translates the current matrix. It does this by first creating a matrix and then multiplying that matrix with the current matrix
Multiplying one matrix by another, means that the 6 values or 3 vectors for X Axis, Y Axis, and Origin are transform by the other matrix.
If written as code
const current = [1,0,0,1,0,0]; // Default matrix
function translate(x, y) { // Translate current matrix
const translationMatrix = [1,0,0,1,x,y];
const c = current
const m = translationMatrix
const r = []; // the resulting matrix
r[0] = c[0] * m[0] + c[1] * m[2]; // rotate current X Axis with new transform
r[1] = c[0] * m[1] + c[1] * m[3];
r[2] = c[2] * m[0] + c[3] * m[2]; // rotate current Y Axis with new transform
r[3] = c[2] * m[1] + c[3] * m[3];
r[4] = c[4] + m[4]; // Translate current origine with transform
r[5] = c[5] + m[5];
c.length = 0;
c.push(...r);
}
That is the simple version. Under the hood you can not multiply the two matrix as they have different dimensions. The actual matrix is stored as 9 values and requires 27 multiplications and 18 additions
// The real 2D default matrix
const current = [1,0,0,0,1,0,0,0,1];
// The real Translation matrix
const translation = [1,0,0,0,1,0,x,y,1];
//The actual transformation calculation
const c = current
const m = translationMatrix
const r = []; // the resulting matrix
r[0] = c[0] * m[0] + c[1] * m[3] + c[2] * m[6];
r[1] = c[0] * m[1] + c[1] * m[4] + c[2] * m[7];
r[2] = c[0] * m[2] + c[1] * m[5] + c[2] * m[8];
r[3] = c[3] * m[0] + c[4] * m[3] + c[5] * m[6];
r[4] = c[3] * m[1] + c[4] * m[4] + c[5] * m[7];
r[5] = c[3] * m[2] + c[4] * m[5] + c[5] * m[8];
r[6] = c[6] * m[0] + c[7] * m[3] + c[8] * m[6];
r[7] = c[6] * m[1] + c[7] * m[4] + c[8] * m[7];
r[8] = c[6] * m[2] + c[7] * m[5] + c[8] * m[8];
That's a bucket load of math that is always done under the hood when you use ctx.translate
and NOTE that this math is not done on the GPU, it is done on the CPU and the resulting matrix is moved to the GPU.
If we continue the renaming
ctx.translate(x, y); // should be ctx.matrixMutliplyTranslate(
ctx.scale(scaleY, scaleX); // should be ctx.matrixMutliplyScale(
ctx.rotate(angle); // should be ctx.matrixMutliplyRotate(
ctx.transform(a,b,c,d,e,f) // should be ctx.matrixMutliplyTransform(
It is common for JS scripts to use the above function to scale translate and rotates, usually with reverse rotations and translations because their objects are not defined around there local origins.
Thus when you do the following
ctx.rotate(angle);
ctx.scale(sx, sy);
ctx.translate(x, y);
The under the hood math must do all of the following
// create rotation matrix
rr = [Math.cos(rot), Math.sin(rot), 0, -Math.sin(rot), Math.cos(rot), 0, 0, 0, 1];
// Transform the current matix with the rotation matrix
r[0] = c[0] * rr[0] + c[1] * rr[3] + c[2] * rr[6];
r[1] = c[0] * rr[1] + c[1] * rr[4] + c[2] * rr[7];
r[2] = c[0] * rr[2] + c[1] * rr[5] + c[2] * rr[8];
r[3] = c[3] * rr[0] + c[4] * rr[3] + c[5] * rr[6];
r[4] = c[3] * rr[1] + c[4] * rr[4] + c[5] * rr[7];
r[5] = c[3] * rr[2] + c[4] * rr[5] + c[5] * rr[8];
r[6] = c[6] * rr[0] + c[7] * rr[3] + c[8] * rr[6];
r[7] = c[6] * rr[1] + c[7] * rr[4] + c[8] * rr[7];
r[8] = c[6] * rr[2] + c[7] * rr[5] + c[8] * rr[8];
// STOP the GPU and send the resulting matrix over the bus to set new state
c = [...r]; // set the current matrix
// create the scale matrix
ss = [scaleX, 0, 0, 0, scaleY, 0, 0, 0, 1];
// scale the current matrix
r[0] = c[0] * ss[0] + c[1] * ss[3] + c[2] * ss[6];
r[1] = c[0] * ss[1] + c[1] * ss[4] + c[2] * ss[7];
r[2] = c[0] * ss[2] + c[1] * ss[5] + c[2] * ss[8];
r[3] = c[3] * ss[0] + c[4] * ss[3] + c[5] * ss[6];
r[4] = c[3] * ss[1] + c[4] * ss[4] + c[5] * ss[7];
r[5] = c[3] * ss[2] + c[4] * ss[5] + c[5] * ss[8];
r[6] = c[6] * ss[0] + c[7] * ss[3] + c[8] * ss[6];
r[7] = c[6] * ss[1] + c[7] * ss[4] + c[8] * ss[7];
r[8] = c[6] * ss[2] + c[7] * ss[5] + c[8] * ss[8];
// STOP the GPU and send the resulting matrix over the bus to set new state
c = [...r]; // set the current matrix
// create the translate matrix
tt = [1, 0, 0, 0, 1, 0, x, y, 1];
// translate the current matrix
r[0] = c[0] * tt[0] + c[1] * tt[3] + c[2] * tt[6];
r[1] = c[0] * tt[1] + c[1] * tt[4] + c[2] * tt[7];
r[2] = c[0] * tt[2] + c[1] * tt[5] + c[2] * tt[8];
r[3] = c[3] * tt[0] + c[4] * tt[3] + c[5] * tt[6];
r[4] = c[3] * tt[1] + c[4] * tt[4] + c[5] * tt[7];
r[5] = c[3] * tt[2] + c[4] * tt[5] + c[5] * tt[8];
r[6] = c[6] * tt[0] + c[7] * tt[3] + c[8] * tt[6];
r[7] = c[6] * tt[1] + c[7] * tt[4] + c[8] * tt[7];
r[8] = c[6] * tt[2] + c[7] * tt[5] + c[8] * tt[8];
// STOP the GPU and send the resulting matrix over the bus to set new state
c = [...r]; // set the current matrix
So that is a total of 3 GPU state changes, 81 floating point multiplications, 54 floating point additions, 4 high level math calls and about 0.25K RAM allocated and dumped for GC to clean up.
The function setTransform
does not multiply matrices. It converts the 6 arguments to a 3 by 3 matrix by directly putting the values into the current transform and the moving it to the GPU
// ct is the current transform 9 value under hood version
// The 6 arguments of the ctx.setTransform call
ct[0] = a;
ct[1] = b;
ct[2] = 0;
ct[3] = c;
ct[4] = d;
ct[5] = 0;
ct[6] = e;
ct[7] = f;
ct[8] = 1;
// STOP the GPU and send the resulting matrix over the bus to set new state
So if you use the JS function
function createTransform(originX, originY, scale, rotate) {
const xAxisX = Math.cos(rotate) * scale;
const xAxisY = Math.sin(rotate) * scale;
ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, originX, originY);
}
You reduce the complexity under the hood to 2 floating point multiplications, 2 high level math function calls, 1 floating point addition (negating the -xAxisY
), one GPU state change, and using only 64 bytes of RAM from the heap.
And because the ctx.setTransform
does not depend on the current state of the 2D transform you don't need to use ctx.resetTransform
, or ctx.save
and restore
When animating many items the performance benefit is noticeable. When struggling with the complexity of transformed matrices the simplicity of setTransform
can save you hours of time better spend creating good content.
Upvotes: 4
Reputation: 57425
The problem is that after each translation in Circle.draw()
, the context is not restored to its original state. Future translate(this.x, this.y);
calls keep moving the context right and downward relative to the previous transformation endlessly.
Use ctx.save()
and ctx.restore()
at the beginning and end of your draw()
function to move the context back to its original location after drawing.
class Circle {
constructor(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
}
draw() {
ctx.save();
ctx.strokeStyle = "white";
ctx.translate(this.x, this.y);
ctx.beginPath();
ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
let canvas;
let ctx;
let circle;
(function init() {
canvas = document.querySelector("canvas");
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
circle = new Circle(canvas.width / 2, canvas.height / 2, 30);
loop();
})();
function loop() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
circle.draw();
requestAnimationFrame(loop);
}
body {
margin: 0;
height: 100vh;
}
<canvas></canvas>
Alternately, you can just write:
ctx.strokeStyle = "white";
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
and skip the translation step entirely.
Upvotes: 1
Reputation: 2198
In your code, the ctx.translate(0, 0)
does absolutely nothing, because that function sets transformation relative to current transformation. You are telling the context "move 0 pixels right and 0 pixels down". You could fix that by changing the line to ctx.translate(-this.x, -this.y)
so you do the opposite transformation.
However, usually, this is done by saving the context state with CanvasRenderingContext2D.save
before making transformations and then restoring it with CanvasRenderingContext2D.restore
. In your example, it would look like this:
ctx.save(); // here, we are saving state of the context
ctx.strokeStyle = "white";
ctx.translate(this.x, this.y);
ctx.beginPath();
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
ctx.restore(); // after this, context will have the state it had when we called save()
This way is good in cases when you want to return the context to its original state after the operation, rather than the default state (which you usually do when making more complex operations), and when you do multiple transformations which would be complicated to revert.
Upvotes: 0
Reputation: 559
I just found the answer. As @mpen commented ctx.translate(0, 0)
doesnt reset the translation, but this does: ctx.setTransform(1, 0, 0, 1, 0, 0);
. The ctx.translate function translates related to the previous translation.
Upvotes: 1