Html5 canvas - Translate function behaving weirdly - javascript

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.
Edit:
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.

Answer
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.
Understanding the 2D transformation
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 Y
yAxisX, yAxisY are Y Axis X, Y Axis Y
originX, originY are the canvas real pixel coordinates of the origin
The 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
Translation
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,
}
Scale
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
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
Uniform transformation
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,
}
Rotate, scale, and translate
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);
}
Setting or Multiplying the transform.
I will rename this section to
WHY YOU SHOULD AVOID 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.
Easy and Fast
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.

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.

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.

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.

Related

How to get axis-aligned bounding box of an ellipse with all given parameters?

void ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle [, anticlockwise]);
The canvas context 2D API ellipse() method creates an elliptical arc centered at (x, y) with the radii radiusX and radiusY. The path starts at startAngle and ends at endAngle, and travels in the direction given by anticlockwise.
How to get the axis-aligned bounding box of a ellipse with the given parameters:x, y, radiusX, radiusY, rotation, startAngle, endAngle , anticlockwise?
Two solutions
This answer contains two exact solutions it is not an approximation.
The solutions are
boundEllipseAll will find the bounds of the full ellipse. If is a lot less complex than the full solution but you need to ensure that the x radius is greater than the y radius (eg rotate the ellipse 90 deg and swap the x, y radius)
boundEllipse Will find the bounds for a segment of an ellipse. It will work for all ellipses however I have not included the CCW flag. To get the bounds for CCW ellipse swap the start and end angles.
It works by first finding the x, y coords at the start and end points calculating the min and max along each axis. It then calculates the extrema angles, first for the x axis extremes, then the y axis extremes.
If the extrema angle is between the start and end angle, the x,y position of that angle is calculated and the point tested against the min and max extent.
There is a lot of room to optimize as many of the points need only the x, or y parts, and the inner while loop in function extrema can exit early if min and max are change for the axis it is working on.
Example
The example ensures I have not made any mistakes, and uses the second solution, animating an ellipse by moving the start and end angles, rotation, and y axis radius. Drawing the bounding box and the ellipse it bounds.
Update April 2022
Example shows use of both full ellipse boundEllipseAll and ellipse segment boundEllipse.
Note that boundEllipse is only for ellipse segment where endAngle n and startAngle m fit the rule {m <= n <= m + 2Pi}
Fixed bug in boundEllipse that did not show full ellipse when endAngle == startAngle + 2 * Math.PI
const ctx = canvas.getContext("2d");
const W = 200, H= 180;
const TAU = Math.PI * 2;
const ellipse = {
x: W / 2,
y: H / 2,
rx: W / 3,
ry: W / 3,
rotate: 0,
startAng: 0,
endAng: Math.PI * 2,
dir: false,
};
function boundEllipseAll({x, y, rx, ry, rotate}) {
const xAx = Math.cos(rotate);
const xAy = Math.sin(rotate);
const w = ((rx * xAx) ** 2 + (ry * xAy) ** 2) ** 0.5;
const h = ((rx * xAy) ** 2 + (ry * xAx) ** 2) ** 0.5;
return {x: -w + x, y: -h + y, w: w * 2, h: h * 2};
}
function boundEllipse({x, y, rx, ry, rotate, startAng, endAng}) {
const normalizeAng = ang => (ang % TAU + TAU) % TAU;
const getPoint = ang => {
const cA = Math.cos(ang);
const sA = Math.sin(ang);
return [cA * rx * xAx - sA * ry * xAy, cA * rx * xAy + sA * ry * xAx];
}
const extrema = a => { // from angle
var i = 0;
while(i < 4) {
const ang = normalizeAng(a + Math.PI * (i / 2));
if ((ang > startAng && ang < endAng) || (ang + TAU > startAng && ang + TAU < endAng)) {
const [xx, yy] = getPoint(ang);
minX = Math.min(minX, xx);
maxX = Math.max(maxX, xx);
minY = Math.min(minY, yy);
maxY = Math.max(maxY, yy);
}
i ++;
}
}
// UPDATE bug fix (1) for full ellipse
const checkFull = startAng !== endAng; // Update fix (1)
startAng = normalizeAng(startAng);
endAng = normalizeAng(endAng);
(checkFull && startAng === endAng) && (endAng += TAU); // Update fix (1)
const xAx = Math.cos(rotate);
const xAy = Math.sin(rotate);
endAng += endAng < startAng ? TAU : 0;
const [sx, sy] = getPoint(startAng);
const [ex, ey] = getPoint(endAng);
var minX = Math.min(sx, ex);
var maxX = Math.max(sx, ex);
var minY = Math.min(sy, ey);
var maxY = Math.max(sy, ey);
extrema(-Math.atan((ry * xAy) / (rx * xAx))); // Add x Axis extremas
extrema(-Math.atan((rx * xAy) / (ry * xAx))); // Add y Axis extremas
return {x: minX + x, y: minY + y, w: maxX - minX, h: maxY - minY};
}
function drawExtent({x,y,w,h}) {
ctx.moveTo(x,y);
ctx.rect(x, y, w, h);
}
function drawEllipse({x, y, rx, ry, rotate, startAng, endAng, dir}) {
ctx.ellipse(x, y, rx, ry, rotate, startAng, endAng, dir);
}
function drawFullEllipse({x, y, rx, ry, rotate, dir}) {
ctx.ellipse(x, y, rx, ry, rotate, 0, TAU, dir);
}
mainLoop(0);
function mainLoop(time) {
ctx.clearRect(0, 0, W, H);
// Animate ellipse
ellipse.startAng = time / 1000;
ellipse.endAng = time / 2000;
ellipse.rotate = Math.cos(time / 14000) * Math.PI * 2;
ellipse.ry = Math.cos(time / 6000) * (W / 4 - 10) + (W / 4);
// Draw full ellipse and bounding box.
ctx.strokeStyle = "#F008";
ctx.beginPath();
drawFullEllipse(ellipse);
drawExtent(boundEllipseAll(ellipse));
ctx.stroke();
// Draw ellipse segment and bounding box.
ctx.strokeStyle = "#0008";
ctx.beginPath();
drawEllipse(ellipse);
drawExtent(boundEllipse(ellipse));
ctx.stroke();
requestAnimationFrame(mainLoop)
}
canvas { border: 1px solid black }
<canvas id="canvas" width="200" height="180"></canvas>

how i get a coordinate of each rectangle?

i have tried this,
public drawNumbers(ctx, x1, y1, length, count) {
let angle = 0;
for (let i = 0; i <= count; i++ ) {
angle += 2 * Math.PI / (count );
const x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
ctx.beginPath();
ctx.fillRect(x2, y2, 10, 20);
ctx.stroke();
}
}
this.canvas.drawNumbers(ctx, this.midX, this.midY, 160, 60);
output:
expected result:
i want to calculate a four coordinate(rectangle) of rotated axis.
How do i detect click event on each rectangle?
Using setTransform
Salix alba answer is a solution though a few too many steps.
It can be done in a single transform using setTransform and applying the translate and rotations in one step. Also the second translation is where you draw the box relative to its origin. When using transforms always draw objects around the center of rotation.
ctx.strokeRect(-10,-10,20,20); // rotation is always around 0,0
const ctx = canvas.getContext("2d");
const centerX = 250;
const centerY = 250;
const radius = 200;
const boxWidth = 10;
const bobLength = 20;
// draw boxs around circle center at cx,cy and radius rad
// box width bw, and box height bh
// spacing optional is the distance between boxes
function drawCircleOfBoxes(cx,cy,rad,bw,bh,spacing = 5){
var steps = ((rad - bw /2) * Math.PI * 2) / (bw + spacing) | 0; // get number boxes that will fit circle
ctx.beginPath();
for(var i = 0; i < steps; i ++){
const ang = (i / steps) * Math.PI * 2;
var xAxisX = Math.cos(ang); // get the direction of he xAxis
var xAxisY = Math.sin(ang);
// set the transform to circle center x Axis out towards box
// y axis at 90 deg to x axis
ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, cx, cy);
// draw box offset from the center so its center is distance radius
ctx.rect(rad - bh / 2, -bw / 2, bh, bw);
}
ctx.fill();
ctx.stroke();
ctx.setTransform(1,0,0,1,0,0); // reset transform
}
ctx.fillStyle = "#FCD";
ctx.strokeStyle = "#000";
drawCircleOfBoxes(centerX, centerY, radius, boxWidth, bobLength);
<canvas id="canvas" width="500" height="500"></canvas>
Manually apply the transform to a point
If you wish to transform the box in code you can use the transform applied in the above and apply it directly to a set of points. You can not apply it to the ctx.rect function that needs the API transform.
To transform a point px,py you need the the direction of the rotated x axis
const xAx = Math.cos(dirOfXAxis);
const xAy = Math.sin(dirOfXAxis);
You can then move the point px distance along the xAxis and then turn 90 deg and move py distance along the y axis
var x = px * xAx; // move px dist along x axis
var y = px * xAy;
x += py * -xAy; // move px dist along y axis
y += py * xAx;
Then just add the translation
x += translateX;
y += translateY;
Or done in one go
var x = px * xAx - py * xAy + translateX; // move px dist along x axis
var y = px * xAy + py * xAx + translateY;
The snippet shows it in action
const ctx = canvas.getContext("2d");
const centerX = 250;
const centerY = 250;
const radius = 200;
const boxWidth = 10;
const boxLength = 20;
// draw boxs around circle center at cx,cy and radius rad
// box width bw, and box height bh
// spacing optional is the distance between boxes
function drawCircleOfBoxes(cx,cy,rad,bw,bh,spacing = 5){
var points = [ // setout points of box with coord (0,0) as center
{x : bh / 2, y : -bw / 2},
{x : bh / 2 + bh, y : -bw / 2},
{x : bh / 2 + bh, y : -bw / 2 + bw},
{x : bh / 2, y : -bw / 2 + bw},
];
var steps = (((rad - bw /2) * Math.PI * 2) / (bw + spacing) )+ 4| 0; // get number boxes that will fit circle
ctx.beginPath();
for(var i = 0; i < steps; i ++){
const ang = (i / steps) * Math.PI * 2;
const xAx = Math.cos(ang); // get the direction of he xAxis
const xAy = Math.sin(ang);
var first = true
for(const p of points){ // for each point
// Apply the transform to the point after moving it
// to the circle (the p.x + rad)
const x = (p.x + rad) * xAx - p.y * xAy + cx;
const y = (p.x + rad) * xAy + p.y * xAx + cy;
if(first){
ctx.moveTo(x,y);
first = false;
}else{
ctx.lineTo(x,y);
}
}
ctx.closePath();
}
ctx.fill();
ctx.stroke();
}
ctx.fillStyle = "#CFD";
ctx.strokeStyle = "#000";
for(var i = boxLength + 5; i < radius; i += boxLength + 5){
drawCircleOfBoxes(centerX, centerY, i , boxWidth, boxLength);
}
<canvas id="canvas" width="500" height="500"></canvas>
To get rotated rectangles you need to use the transform() method of the graphics context.
Imagine a set of axis at the top left of the drawing area. Any drawing will be done relative to these axis which we can move with transform.
To translate by xshift, yshift
ctx.transform(1,0,0,1, xshift, yshift);
ctx.fillRect(0,0,100,100);
To rotate by angle ang in radians
ctx.transform(Math.cos(ang),Math.sin(ang),
-Math.sin(ang),Math.cos(ang), 0,0);
We can combine things with three transformations. The first moves the origin to the center of the circle. Then rotate the axes about this point,
then shift the axes to where you want the shape to appear. Finally, draw the shape.
for(deg = 0; deg < 360; deg+=20) {
ctx.setTransform(1,0,0,1,0,0); // reset transformation
ang = deg * Math.PI/180;
ctx.transform(1,0,0,1,100,100); // shift origin
ctx.transform(Math.cos(ang),Math.sin(ang),
-Math.sin(ang),Math.cos(ang), 0,0);
ctx.transform(1,0,0,1,50,0);
ctx.fillRect(0,0,30,10);
}
You can achieve the same this using the translate and rotate
for(deg = 0; deg < 360; deg+=20) {
ctx.setTransform(1,0,0,1,0,0); // reset transformation
ang = deg * Math.PI/180;
ctx.translate(100,100); // shift origin
ctx.rotate(ang);
ctx.translate(50,0);
ctx.fillRect(0,0,30,10);
}

3d sphere using javascript

I have been trying to learn creating basic 3d sphere using html5 canvas and I am new to 3d programming. When I googled, I found this incredibly useful article.You can find working example there.
function drawEllipse(ox, oy, oz, or, alpha, beta, gamma, hidden) {
for (var theta=0; theta <=2 * Math.PI; theta += 0.02) {
var px = ox + or * Math.cos(theta);
var py = oy;
var pz = oz + or * Math.sin(theta);
var a = transform(px, py, pz, alpha, beta, gamma);
if (!hidden || a[2] >= 0) {
drawDot(radius * a[0], radius * a[1]);
}
}
}
function drawDot(x, y) {
ctx.fillRect(x, y, 1, 1);
}
/* 3d rotation matrix */
function transform(x, y, z, rz, rx, ry) {
return [
((x * Math.sin(rz) - y * Math.cos(rz)) * Math.sin(rx) + z * Math.cos(rx)) * Math.sin(ry) + (x * Math.cos(rz) - y * Math.sin(rz)) * Math.cos(ry),
(x * Math.sin(rz) - y * Math.cos(rz)) * Math.cos(rx) - z * Math.sin(rx),
((x * Math.sin(rz) - y * Math.cos(rz)) * Math.sin(rx) + z * Math.cos(rx)) * Math.cos(ry) - (x * Math.cos(rz) - y * Math.sin(rz)) * Math.sin(ry)
]
}
/*latitude circles*/
for (var latitude = 0; latitude < Math.PI / 2; latitude += Math.PI / 18) {
drawEllipse(0, Math.sin(latitude), 0,Math.cos(latitude),0, alpha, gamma,true);
drawEllipse(0, -Math.sin(latitude), 0,Math.cos(latitude),0, alpha, gamma,true);
}
/*longitude circles*/
for (var longitude=0 ; longitude < Math.PI; longitude += Math.PI / 18) {
drawEllipse(0, 0, 0,1,longitude, Math.PI / 2 + alpha, gamma,true);
} // alpha and gamma are the x and y axis rotation angles
But the only problem is the direction of rotation when dragging the mouse on the sphere. Yaw and Pitch direction is not correct. Rotating left to right(yaw) working fine. But rotating top to bottom(pitch) is different. Can anyone please find a solution ?

How to "push out "XYZ coordinates forming a 3D orbit with an offset in the middle

I have a orbit of length 200. But it is centered around a sun of radius 0 (length 0). Now I want to expand the sun to have a radius of 1 and "push" out the outer orbits as well.
The XYZ coordinates look like this:
[-6.76, 5.75, -1.06],
[-6.95, 5.54, -0.91],
[-7.13, 5.33, -0.75],
[-7.31, 5.11, -0.58]
... followed by 196 more coordinates
I tried tried a lot of things to make the circle bigger * radius and / someNumbers. To at least try to do it myself.
But i lost it when i made an if like this:
If(the x coordination > 0)
the x coordination += 1;
}
Else{
the x coordination += 1;
}
And also for Y and Z but when they came close to the 1 and -1 position of that axis they skipped to the other side.
Creating a line (with the width of 1 on both sides) of emptiness along the axis.
Result of MBo's awnser(view from above):
// arrayIndex is a number to remember at which point it is in the orbit array
satellites.forEach(function (element) {
if (element.arrayIndex>= element.satellite.coordinates.length) {
element.arrayIndex= 0;
}
var posX = element.satellite.coordinates[element.arrayIndex][0];
var posY = element.satellite.coordinates[element.arrayIndex][1];
var posZ = element.satellite.coordinates[element.arrayIndex][2];
R = Math.sqrt(posX^2 + posY^2 + posZ^2);
cf = (R + earthRadius) / R;
xnew = posX * cf;
ynew = posY * cf;
znew = posZ * cf;
// var posX = earthRadius * (element.satellite.coordinates[element.test][0] / (200 * earthRadius) * earthRadius);
// var posY = earthRadius * (element.satellite.coordinates[element.test][1] / (200 * earthRadius) * earthRadius);
// var posZ = earthRadius * (element.satellite.coordinates[element.test][2] / (200 * earthRadius) * earthRadius);
// divide by 100 to scale it down some more
element.position.x = xnew / 100;
element.position.y = ynew / 100;
element.position.z = znew / 100;
element.arrayIndex= element.arrayIndex+ 1;
});
You have orbit radius
/////////R = Sqrt(x^2 + y^2 + z^2)
Edit to avoid confusion:
R = Sqrt(x * x + y * y + z * z)
You need to modify coordinates to make orbit radius R+r. To preserve orbit form, for every point find it's R, and multiply all components by coefficient (R+r)/R
R = Sqrt(x^2 + y^2 + z^2)
cf = (R + r) / R
xnew = x * cf
ynew = y * cf
znew = z * cf

Create equilateral triangle in the middle of canvas?

I want to draw an equilateral triangle in the middle of canvas. I tried this:
ctx.moveTo(canvas.width/2, canvas.height/2-50);
ctx.lineTo(canvas.width/2-50, canvas.height/2+50);
ctx.lineTo(canvas.width/2+50, canvas.height/2+50);
ctx.fill();
But the triangle looks a bit too tall.
How can I draw an equilateral triangle in the middle of canvas?
Someone told me you have to find the ratio of the height of an equilateral triangle to the side of an equilateral triangle.
h:s
What are the two numbers?
The equation for the three corner points is
x = r*cos(angle) + x_center
y = r*sin(angle) + y_center
where for angle = 0, (1./3)*(2*pi), and (2./3)*(2*pi); and where r is the radius of the circle in which the triangle is inscribed.
You have to do it with the height of the triangle
var h = side * (Math.sqrt(3)/2);
or
var h = side * Math.cos(Math.PI/6);
So the ratio h:s is equal to:
sqrt( 3 ) / 2 : 1 = cos( π / 6 ) : 1 ≈ 0.866025
See : http://jsfiddle.net/rWSKh/2/
A simple version where X and Y are the points you want to top of the triangle to be:
var height = 100 * (Math.sqrt(3)/2);
context.beginPath();
context.moveTo(X, Y);
context.lineTo(X+50, Y+height);
context.lineTo(X-50, Y+height);
context.lineTo(X, Y);
context.fill();
context.closePath();
This makes an equilateral triange with all sides = 100. Replace 100 with how long you want your side lengths to be.
After you find the midpoint of the canvas, if you want that to be your triangle's midpoint as well you can set X = midpoint's X and Y = midpoint's Y - 50 (for a 100 length triangle).
The side lengths will not be equal given those coordinates.
The horizontal line constructed on the bottom has a length of 100, but the other sides are actually the hypotenuse of a 50x100 triangle ( approx. 112).
I can get you started with drawing an equilateral triangle but I don't have the time to get it centered.
jsFiddle
var ax=0;
var ay=0;
var bx=0;
var by=150;
var dx=bx-ax
var dy=by-ay;
var dangle = Math.atan2(dy, dx) - Math.PI / 3;
var sideDist = Math.sqrt(dx * dx + dy * dy);
var cx = Math.cos(dangle) * sideDist + ax;
var cy = Math.sin(dangle) * sideDist + ay;
var canvas = document.getElementById('equ');
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(ax,ay);
ctx.lineTo(bx,by);
ctx.lineTo(cx,cy);
ctx.fill();
my code for drawing triangle also depending on direction (for lines). code is for Raphael lib.
drawTriangle(x2 - x1, y2 - y1, x2, y2);
function drawTriangle(dx, dy, midX, midY) {
var diff = 0;
var cos = 0.866;
var sin = 0.500;
var length = Math.sqrt(dx * dx + dy * dy) * 0.8;
dx = 8 * (dx / length);
dy = 8 * (dy / length);
var pX1 = (midX + diff) - (dx * cos + dy * -sin);
var pY1 = midY - (dx * sin + dy * cos);
var pX2 = (midX + diff) - (dx * cos + dy * sin);
var pY2 = midY - (dx * -sin + dy * cos);
return [
"M", midX + diff, midY,
"L", pX1, pY1,
"L", pX2, pY2,
"L", midX + diff, midY
].join(",");
}

Categories

Resources