How to centre align my line drawing using Canvas? - javascript

Problem
I am trying to put this line drawing in the center of my canvas, when I try to use the moveTo(100, 400) for the x-axis, it does not change the horizontal start position to 100. If I try the same thing with the y-axis it will move the the line along the x-axis.
I also need help with drawing the y-axis numbers 1 - 9 vertically along the y-axis it seems to only align horizontally.
EDIT!: I have manually stroked each point on the y-axis so I have the numbers on there, now I just want to know how to move the graph to center!!
Script
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.linecap = 'round';
// draw a scale with the numbers on it
ctx.lineWidth = 2;
ctx.strokeStyle = '#FF9900';
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.moveTo(100, 400);
for (i = 0; i <= 6; i+=1) {
//put a stroke mark
ctx.lineTo(100*i,400);
ctx.lineTo(100*i,405); //markers
ctx.lineTo(100*i,400);
// write the number 10px below
ctx.strokeStyle = '#000000';
// default size is 10px
ctx.strokeText(i, 100*i, 415);
ctx.strokeStyle = '#FF9900';
}
// draw a vertical scale with lines on it
ctx.moveTo(0, -100);
for (b = 0; b <= 9; b+=1) {
//put a stroke mark
ctx.lineTo(0,44.5*b);
ctx.lineTo(5,44.5*b);
ctx.lineTo(0,44.5*b);
// write the number 10px below
ctx.strokeStyle = '#000000';
// default size is 10px
}
ctx.strokeStyle = '#000000'
ctx.strokeText(1, 8, 365);
ctx.strokeText(2, 8, 320.5);
ctx.strokeText(3, 8, 276);
ctx.strokeText(4, 8, 231.5);
ctx.strokeText(5, 8, 187);
ctx.strokeText(6, 8, 142.5);
ctx.strokeText(7, 8, 98);
ctx.strokeText(8, 8, 53.5);
ctx.strokeText(9, 8, 9);
ctx.strokeStyle = '#FF9900';
ctx.stroke();
<!DOCTYPE html>
<html>
<head>
<title>Canvas Axis calibration</title>
<link rel="stylesheet" type="text/css" href="base.css"/>
</head>
<body>
<canvas id="myCanvas" width="1600" height="500"style="border:1px solid #c3c3c3;">
Canvas is not playing!
</canvas>
</body>
</html>

moveTo() just set starting point for your line, it's not draw actual line. Use lineTo() for draw actual line. so moveTo() is from or where you begin and lineTo() is where you go. So starting point for x axis must be moveTo(800, 0).
var c = document.getElementById("myCanvas"),
ctx = c.getContext("2d"),
lineWidth = 2,
xNumber = 6,
yNumber = 9,
xCenter = c.width / 2,
yCenter = 44.5 * yNumber + 44.5
ctx.linecap = 'round';
// draw a scale with the numbers on it
ctx.lineWidth = lineWidth;
ctx.strokeStyle = '#FF9900';
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.moveTo(xCenter, yCenter);
for (i = 0; i <= xNumber; ++i) {
//put a stroke mark
ctx.lineTo((xCenter + (100 * i)), yCenter);
ctx.lineTo((xCenter + (100 * i)), (yCenter + 5)); //markers
ctx.lineTo((xCenter + (100 * i)), yCenter);
// write the number 10px below
ctx.strokeStyle = '#000000';
// default size is 10px
ctx.strokeText(i, (xCenter + (100 * i)), (yCenter + 15));
}
ctx.strokeStyle = '#FF9900';
ctx.stroke()
// draw a vertical scale with lines on it
ctx.beginPath()
ctx.moveTo(xCenter, yCenter);
for (b = 0; b <= yNumber; ++b) {
//put a stroke mark
if(b === 0) continue;
ctx.lineTo(xCenter, (yCenter - (44.5 * b)));
ctx.lineTo((xCenter - 5), (yCenter - (44.5 * b)));
ctx.lineTo(xCenter, (yCenter - (44.5 * b)));
ctx.strokeStyle = '#000000';
ctx.strokeText(b, (xCenter - 15), (yCenter - (44.5 * b)));
}
ctx.strokeStyle = '#FF9900';
ctx.stroke();
<!DOCTYPE html>
<html>
<head>
<title>Canvas Axis calibration</title>
<link rel="stylesheet" type="text/css" href="base.css"/>
</head>
<body>
<canvas id="myCanvas" width="1600" height="500"style="border:1px solid #c3c3c3;">
Canvas is not playing!
</canvas>
</body>
</html>

CanvasRenderingContext2D has a method for that: translate(). It simply sets a coordinate-shift which is going to be applied to everything you draw afterwards:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.linecap = 'round';
ctx.lineWidth = 2;
ctx.fillStyle = 'blue';
ctx.translate((1600-500)/2,0); // <-----------
ctx.strokeStyle = '#000000';
ctx.beginPath();
ctx.moveTo(100, 400);
for (var i = 0; i <= 6; i+=1) {
ctx.lineTo(100*i,400);
ctx.lineTo(100*i,405);
ctx.lineTo(100*i,400);
ctx.strokeText(i, 100*i, 415);
}
ctx.moveTo(0, -100);
for (var b = 0; b <= 9; b+=1) {
ctx.lineTo(0,44.5*b);
ctx.lineTo(5,44.5*b);
ctx.lineTo(0,44.5*b);
if(b<9)
ctx.strokeText(b+1, 8, 365-44.5*b);
}
ctx.strokeStyle = '#FF9900';
ctx.stroke();
<!DOCTYPE html>
<html>
<head>
<title>Canvas Axis calibration</title>
<link rel="stylesheet" type="text/css" href="base.css"/>
</head>
<body>
<canvas id="myCanvas" width="1600" height="500"style="border:1px solid #c3c3c3;">Canvas is not playing!</canvas>
</body>
</html>
Here I assumed the drawing is 500 units wide, which does not seem to be entirely correct, but you will certainly see the result of translate(). The effect of translate() can be reset with setTransform(1, 0, 0, 1, 0, 0) call (if you are familiar with homogeneous coordinates and transformation matrices, note it has a heavily modified order, see in docs). It is actually a matrix which can do all kinds of 2D transformations (translation, rotation, scaling, skewing), translate() is just a convenience function (the equivalent call probaby would be setTransform(1,0,0,1,(1600-500)/2,0), but I have not tried).
Minor changes:
added the var-s into the loops: otherwise variables become global ones which is usually not a problem for a loop variable like i, but generally considered bad practice
reduced to a single stroke() and two strokeStyle-s. The thing is that lines, arcs and the like are drawn with the settings which are set at the very moment when you call stroke(), it does not matter what happened in between. So color is black for most of the time, as strokeText() is immediate, and color becomes that beige/whatever one just for the stroke()
moved the second set of labels into the corresponding loop. I am not sure if the loop is entirely correct, as 9 labels and 9 line segments are visible, but 10 line segments are drawn.

Think local origins
When you get a building plan you don't get a giant sheet of paper with the plan way in the corner because you are building in the burbs, you want to move some windows out of the summer sun, you don't redraw the plan with new coordinates for each wall.
No you get the plan that fits a small sheet, on the plan is a location and orientation. The position of the walls are fix to the local coordinates of the plan.
Same goes for drawing in 2D. You can define a box as 4 points around an origin. [[-10,-10],[10,-10],[10,10],[-10,10]] and when you draw it you set its location and orientation, you dont change the position of each point to the new location.
Draw local coordinate in the world via setTransform
In the 2D API the position and orientation is set via a transform.
function drawPath(x,y, points) { // only position changes
ctx.setTransform(1,0,0,1,x,y); // set the location
ctx.beginPath();
for(const [x,y] of points) {
ctx.lineTo(x,y);
}
ctx.stroke();
}
const box = [[-10,-10],[10,-10],[10,10],[-10,10]];
drawPath(100, 100, box);
And with scale and rotate
function drawPath(x,y,scale, rotate, points) {
const xdx = Math.cos(rotate) * scale;
const xdy = Math.sin(rotate) * scale;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y); // set the location
ctx.beginPath();
for(const [x,y] of points) {
ctx.lineTo(x,y);
}
ctx.stroke();
}
drawPath(100, 100, 2, 0.5, box);
const box = [[-10,-10],[10,-10],[10,10],[-10,10]];
const W = canvas.width;
const H = canvas.height;
const ctx = canvas.getContext("2d");
ctx.font = "2opx arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "red";
const rand = v => Math.random() * v;
drawRandom();
function drawPath(x, y,scale, rotate, points) {
const xdx = Math.cos(rotate) * scale;
const xdy = Math.sin(rotate) * scale;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y); // set the location
ctx.fillText("Hi",0,0);
ctx.beginPath();
for(const [x,y] of points) {
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0); // Resets so line width remains 1 px
ctx.stroke();
}
function drawRandom() {
drawPath(rand(W), rand(H), rand(2) + 0.5, rand(Math.PI * 2), box);
setTimeout(drawRandom, 500);
}
canvas {
border: 1px solid black;
}
<canvas id="canvas" width ="400" height="400"></canvas>
All you need is ctx.setTransform and maybe ctx.transform if you are doing rigged animation stuff. I never use ctx.translate, ctx.scale, ctx.rotate because they are slow, and its hard to picture just where you are, oh and did I say they are SLOW!!!!
To reset the transform (remove scale, rotation and move back to 0,0) call ctx.resetTransform() or ctx.setTransform(1,0,0,1,0,0)
And some more regarding your approach to the code.
Granular coding
Looks like you want to draw a graph.
Manually drawing every tick, setting styles, and dozens of magic numbers and values is not going to make it much fun. Worse is that when it comes time to make changes it will take forever.
Don't repeat
You need to think like a lazy programmer. Create functions so you dont have to do the same thing over and over.
Define styles once and name them
For example setting the 2D context style is a pain. A drawing usually only has a few different styles, so create an object with named styles
const styles = {
textHang: {
textAlign : "center",
textBaseline : "top",
fillStyle: "blue",
font: "16px Arial",
},
};
And a function that will set a style
const setStyle = (style, c = ctx) => Object.assign(c, style);
Now you can set a style
const ctx = myCanvas.getContext("2d");
setStyle(styles, styles.textHang);
ctx.fillText("The text", 100, 100);
Basic 2D point helper
You are working in 2D and 2D uses a lot of points. You will be adding multiplying, copying... 2D points over and over and over.
Reduce the typing and cover the most basic 2D needs with only 7 functions
const P2 = (x = 0, y = 0) => ({x,y});
const P2Set = (p, pAs) => (p.x = pAs.x, p.y = pAs.y, p);
const P2Copy = p => P2(p.x, p.y);
const P2Mult = (p, val) => (p.x *= val, p.y *= val, p);
const P2Add = (p, pAdd) => (p.x += pAdd.x, p.y += pAdd.y, p);
const P2Sub = (p, pSub) => (p.x -= pSub.x, p.y -= pSub.y, p);
const P2Dist = (p1, p2) => ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5;
No line? 2D API
The 2D API is great, but lacking. To just draw a line is crazy long, foo bar....
ctx.linecap = 'round';
ctx.lineWidth = 2;
ctx.strokeStyle = '#FF9900';
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(410, 410);
ctx.stroke();
No way create functions, use named styles, don't enter coordinates use points.
Some common 2D tasks as functions
const clear = (c = ctx) => (setPos(), c.clearRect(0,0,c.canvas.width,c.canvas.height));
const line = (p1, p2, c = ctx) => (c.moveTo(p1.x, p1.y), c.lineTo(p2.x, p2.y))
const setPos = (p, c = ctx) => p ? c.setTransform(1, 0, 0, 1, p.x, p.y) : c.resetTransform();
const path = (p, path, c = ctx) => {
c.setTransform(1,0,0,1,p.x,p.y);
for(const seg of path) { // each segment
let first = true;
for(const p of seg) { // each point
first ? (c.moveTo(p.x,p.y), first = false):(c.lineTo(p.x, p.y));
}
}
}
Example
The following takes all the above and creates 2 Axis. It may seem like a lot extra, but as you add complexity to your drawing you quickly find you need less and less code.
/* Set up the context get common values eg W,H for width and height */
const W = canvas.width;
const H = canvas.height;
const ctx = canvas.getContext("2d");
// Helper functions will use a global ctx, or pass a 2d context as last argument
// P2 is a point. I use p to mean a point
const P2 = (x = 0, y = 0) => ({x,y});
const P2Set = (p, pAs) => (p.x = pAs.x, p.y = pAs.y, p);
const P2Copy = p => P2(p.x, p.y);
const P2Mult = (p, val) => (p.x *= val, p.y *= val, p);
const P2Add = (p, pAdd) => (p.x += pAdd.x, p.y += pAdd.y, p);
const P2Sub = (p, pSub) => (p.x -= pSub.x, p.y -= pSub.y, p);
const P2Dist = (p1, p2) => ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** 0.5;
const setStyle = (style, c = ctx) => Object.assign(c, style);
const clear = (c = ctx) => (setPos(0, c), c.clearRect(0,0,c.canvas.width,c.canvas.height));
const line = (p1, p2, c = ctx) => (c.moveTo(p1.x, p1.y), c.lineTo(p2.x, p2.y))
const setPos = (p, c = ctx) => p ? c.setTransform(1, 0, 0, 1, p.x, p.y) : c.resetTransform();
const path = (p, path, c = ctx) => {
setPos(p,c);
for(const seg of path) { // each segment
let first = true;
for(const p of seg) { // each point
first ? (c.moveTo(p.x,p.y), first = false):(c.lineTo(p.x, p.y));
}
}
}
const styles = { // define any of the 2D context properties you wish to set
textHang: {textAlign : "center", textBaseline : "top"},
textLeft: {textAlign : "left", textBaseline : "middle"},
markTextStyle: {fillStyle: "blue", font: "16px Arial"},
markStyle: {
strokeStyle: "black",
lineCap: "round",
lineWidth: 2,
},
};
const paths = { // Array of arrays of points. each sub array is a line segment
markLeft: [[P2(-2, 0), P2(5, 0)]],
markUp: [[P2(0, 2), P2(0, -5)]],
}
// Draw an axis from point to point, using mark to mark, lineStyle for the line
// marks is an array of names for each mark, markStyle is the style for the text marks
// markDist is the distance out (90 to the right) to put the text marks
function drawAxis(fromP, toP, mark, lineStyle, marks, markStyle, markDist) {
const norm = P2Mult(P2Sub(P2Copy(toP), fromP), 1 / P2Dist(fromP, toP));
const step = P2Mult(P2Sub(P2Copy(toP), fromP), 1 / (marks.length-1));
const pos = P2Copy(fromP);
setStyle(lineStyle);
ctx.beginPath();
setPos(); // without argument pos is 0,0
line(fromP, toP);
for(const m of marks) {
path(pos, mark);
P2Add(pos, step);
}
ctx.stroke();
P2Set(pos, fromP);
setStyle(markStyle);
for(const m of marks) {
setPos(pos);
ctx.fillText(m,-norm.y * markDist, norm.x * markDist)
P2Add(pos, step)
}
}
const insetW = W * 0.1;
const insetH = H * 0.1;
const axisText = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
clear();
drawAxis(
P2(insetW, H - insetH), P2(insetW, insetH), paths.markLeft,
styles.markStyle,
axisText,
{...styles.textLeft, ...styles.markTextStyle},
-18
);
drawAxis(
P2(insetW, H - insetH), P2(W - insetW, H - insetH), paths.markUp,
styles.markStyle,
axisText,
{...styles.textHang, ...styles.markTextStyle},
6
);
canvas {
border: 1px solid black;
}
<canvas id="canvas" width ="400" height="400"></canvas>

Related

How to draw an isometric 3D cube with 3 perfectly identical faces with fillRect?

I would like to create an isometric 3D cube with fillRect whose 3 faces have the same dimensions as the image below:
Edit: I want to do it with fillRect. The reason for this is that I will draw images on the 3 faces of the cube afterwards. This will be very easy to do since I will use exactly the same transformations as for drawing the faces.
Edit 2: I didn't specify that I want to avoid using an external library so that the code is as optimized as possible. I know that it is possible to calculate the 3 matrices beforehand to draw the 3 faces and make a perfect isometric cube.
Edit 3: As my example code showed, I want to be able to set the size of the side of the isometric cube on the fly (const faceSize = 150).
I have a beginning of code but I have several problems:
The faces are not all the same dimensions
I don't know how to draw the top face
const faceSize = 150;
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Top Face (not big enough)
ctx.save();
ctx.translate(centerX, centerY);
ctx.scale(1, .5);
ctx.rotate(-45 * Math.PI / 180);
ctx.fillStyle = 'yellow';
ctx.fillRect(0, -faceSize, faceSize, faceSize);
ctx.restore();
// Left Face (not high enough)
ctx.save();
ctx.translate(centerX, centerY);
ctx.transform(1, .5, 0, 1, 0, 0);
ctx.fillStyle = 'red';
ctx.fillRect(-faceSize, 0, faceSize, faceSize);
ctx.restore();
// Right Face (not high enough)
ctx.save();
ctx.translate(centerX, centerY);
ctx.transform(1, -.5, 0, 1, 0, 0);
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, faceSize, faceSize);
ctx.restore();
<canvas width="400" height="400"></canvas>
I used a large part of #enhzflep's code which I adapted so that the width of the cube is dynamically changeable.
All the code seems mathematically correct, I just have a doubt about the value 1.22 given as a parameter to scaleSelf. Why was this precise value chosen?
Here is the code:
window.addEventListener('load', onLoad, false);
const canvas = document.createElement('canvas');
function onLoad() {
//canvas.width = cubeWidth;
//canvas.height = faceSize * 2;
canvas.width = 400;
canvas.height = 400;
document.body.appendChild(canvas);
drawCube(canvas);
}
function drawCube() {
const scale = Math.abs(Math.sin(Date.now() / 1000) * canvas.width / 200); // scale effect
const faceSize = 100 * scale;
const radians = 30 * Math.PI / 180;
const cubeWidth = faceSize * Math.cos(radians) * 2;
const centerPosition = {
x: canvas.width / 2,
y: canvas.height / 2
};
const ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const defaultMat = [1, 0, 0, 1, 0, 0];
// Left (red) side
const leftMat = new DOMMatrix(defaultMat);
leftMat.translateSelf(centerPosition.x - cubeWidth / 2, centerPosition.y - faceSize / 2);
leftMat.skewYSelf(30);
ctx.setTransform(leftMat);
ctx.fillStyle = '#F00';
ctx.fillRect(0, 0, cubeWidth / 2, faceSize);
// Right (blue) side
const rightMat = new DOMMatrix(defaultMat);
rightMat.translateSelf(centerPosition.x, centerPosition.y);
rightMat.skewYSelf(-30);
ctx.setTransform(rightMat);
ctx.fillStyle = '#00F';
ctx.fillRect(0, 0, cubeWidth / 2, faceSize);
// Top (yellow) side
const topMat = new DOMMatrix(defaultMat);
const toOriginMat = new DOMMatrix(defaultMat);
const fromOriginMat = new DOMMatrix(defaultMat);
const rotMat = new DOMMatrix(defaultMat);
const scaleMat = new DOMMatrix(defaultMat);
toOriginMat.translateSelf(-faceSize / 2, -faceSize / 2);
fromOriginMat.translateSelf(centerPosition.x, centerPosition.y - faceSize / 2);
rotMat.rotateSelf(0, 0, -45);
scaleMat.scaleSelf(1.22, (faceSize / cubeWidth) * 1.22);
topMat.preMultiplySelf(toOriginMat);
topMat.preMultiplySelf(rotMat);
topMat.preMultiplySelf(scaleMat);
topMat.preMultiplySelf(fromOriginMat);
ctx.setTransform(topMat);
ctx.fillStyle = '#FF0';
ctx.fillRect(0, 0, faceSize, faceSize);
ctx.restore();
requestAnimationFrame(drawCube);
}
Here's a quick n dirty approach to the problem. It's too hot here for me to really think very clearly about this question. (I struggle with matrix maths too)
There's 2 things I think worth mentioning, each of which has an effect on the scaling operation.
width and height of the finished figure (and your posted example image) are different.
I think it's the ratio of the distance between (opposite) corners of the untransformed rectangle which fills 1/4 of the canvas, and the finished yellow side which affect the scaling.
Also, note that I'm drawing a square of canvas.height/2 sidelength for the yellow side, whereas I was drawing a rectangle for the red and blue sides.
In the scaling section, width/4 and height/4 are both shorthand for (width/2)/2 and (height/2)/2. width/2 and height/2 give you a rectangle filling 1/2 of the canvas, with a centre (middle of the square) located at (width/2)/2, (height/2)/2 - height/4 means something different in the translation section (even though it's the same number)
With that said, here's the sort of thing I was talking about earlier.
"use strict";
window.addEventListener('load', onLoaded, false);
function onLoaded(evt)
{
let width = 147;
let height = 171;
let canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
document.body.appendChild(canvas);
drawIsoDemo(canvas);
}
function drawIsoDemo(destCanvas)
{
let ctx = destCanvas.getContext('2d');
let width = destCanvas.width;
let height = destCanvas.height;
ctx.fillStyle = '#000';
ctx.fillRect(0,0,width,height);
var idMatVars = [1,0, 0,1, 0,0];
// left (red) side
let leftMat = new DOMMatrix( idMatVars );
leftMat.translateSelf( 0, 0.25*height );
leftMat.skewYSelf(30);
ctx.save();
ctx.transform( leftMat.a, leftMat.b, leftMat.c, leftMat.d, leftMat.e, leftMat.f);
ctx.fillStyle = '#F00';
ctx.fillRect(0,0,width/2,height/2);
ctx.restore();
// right (blue) side
let rightMat = new DOMMatrix( idMatVars );
rightMat.translateSelf( 0.5*width, 0.5*height );
rightMat.skewYSelf(-30);
ctx.save();
ctx.transform( rightMat.a, rightMat.b, rightMat.c, rightMat.d, rightMat.e, rightMat.f);
ctx.fillStyle = '#00F';
ctx.fillRect(0,0,width/2,height/2);
ctx.restore();
// top (yellow) side
let topMat = new DOMMatrix( idMatVars );
let toOriginMat = new DOMMatrix( idMatVars );
let fromOriginMat = new DOMMatrix(idMatVars);
let rotMat = new DOMMatrix(idMatVars);
let scaleMat = new DOMMatrix(idMatVars);
toOriginMat.translateSelf(-height/4, -height/4);
fromOriginMat.translateSelf(width/2,height/4);
rotMat.rotateSelf(0,0,-45);
scaleMat.scaleSelf(1.22,((height/2)/width)*1.22);
topMat.preMultiplySelf(toOriginMat);
topMat.preMultiplySelf(rotMat);
topMat.preMultiplySelf(scaleMat);
topMat.preMultiplySelf(fromOriginMat);
ctx.save();
ctx.transform( topMat.a, topMat.b, topMat.c, topMat.d, topMat.e, topMat.f);
ctx.fillStyle = '#FF0';
ctx.fillRect(0,0,height/2,height/2);
ctx.restore();
}
If we overlay a circle on your isometric cube, we can see that the outer vertices are spaced equally apart. In fact it's always 60°, which is no wonder as it's a hexagon.
So all we have to do is obtaining the coordinates for the outer vertices. This is quite easy as we can make a further assumption: if you look at the shape again, you'll notice that the length of each of the cube's sides seems to be the radius of the circle.
With the help of a little trigonometry and a for-loop which increments by 60 degrees, we can put calculate and put all those vertices into an array and finally connect those vertices to draw the cube.
Here's an example:
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
function drawCube(x, y, sideLength) {
let vertices = [new Point(x, y)];
for (let a = 0; a < 6; a++) {
vertices.push(new Point(x + Math.cos(((a * 60) - 30) * Math.PI / 180) * sideLength, y + Math.sin(((a * 60) - 30) * Math.PI / 180) * sideLength));
}
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.moveTo(vertices[0].x, vertices[0].y);
ctx.lineTo(vertices[5].x, vertices[5].y);
ctx.lineTo(vertices[6].x, vertices[6].y);
ctx.lineTo(vertices[1].x, vertices[1].y);
ctx.lineTo(vertices[0].x, vertices[0].y);
ctx.fill();
ctx.fillStyle = "#a0a0a0";
ctx.beginPath();
ctx.moveTo(vertices[0].x, vertices[0].y);
ctx.lineTo(vertices[1].x, vertices[1].y);
ctx.lineTo(vertices[2].x, vertices[2].y);
ctx.lineTo(vertices[3].x, vertices[3].y);
ctx.lineTo(vertices[0].x, vertices[0].y);
ctx.fill();
ctx.fillStyle = "#efefef";
ctx.beginPath();
ctx.moveTo(vertices[0].x, vertices[0].y);
ctx.lineTo(vertices[3].x, vertices[3].y);
ctx.lineTo(vertices[4].x, vertices[4].y);
ctx.lineTo(vertices[5].x, vertices[5].y);
ctx.lineTo(vertices[0].x, vertices[0].y);
ctx.fill();
}
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
drawCube(200, 150, 85);
canvas {
background: #401fc1;
}
<canvas id="canvas" width="400" height="300"></canvas>
EDIT
What you want to achieve is ain't that easily simply because the CanvasRenderingContext2D API actually does not offer a skewing/shearing transform.
Nevertheless with the help of a third-party library we're able to transform the three sides in an orthographic way. It's called perspective.js
Still we need to calculate the outer vertices but instead of using the moveTo/lineTo commands, we forward the coordinates to perspective.js to actually do the perspective distortion of some source images.
Here's another example:
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
function drawCube(x, y, sideLength) {
let vertices = [new Point(x, y)];
for (let a = 0; a < 6; a++) {
vertices.push(new Point(x + Math.cos(((a * 60) - 30) * Math.PI / 180) * sideLength, y + Math.sin(((a * 60) - 30) * Math.PI / 180) * sideLength));
}
let p = new Perspective(ctx, images[0]);
p.draw([
[vertices[5].x, vertices[5].y],
[vertices[6].x, vertices[6].y],
[vertices[1].x, vertices[1].y],
[vertices[0].x, vertices[0].y]
]);
p = new Perspective(ctx, images[1]);
p.draw([
[vertices[0].x, vertices[0].y],
[vertices[1].x, vertices[1].y],
[vertices[2].x, vertices[2].y],
[vertices[3].x, vertices[3].y]
]);
p = new Perspective(ctx, images[2]);
p.draw([
[vertices[4].x, vertices[4].y],
[vertices[5].x, vertices[5].y],
[vertices[0].x, vertices[0].y],
[vertices[3].x, vertices[3].y]
]);
}
function loadImages(index) {
let image = new Image();
image.onload = function(e) {
images.push(e.target);
if (index + 1 < sources.length) {
loadImages(index + 1);
} else {
drawCube(200, 150, 125, e.target);
}
}
image.src = sources[index];
}
let sources = ["https://picsum.photos/id/1079/200/300", "https://picsum.photos/id/76/200/300", "https://picsum.photos/id/79/200/300"];
let images = [];
loadImages(0);
canvas {
background: #401fc1;
}
<script src="https://cdn.rawgit.com/wanadev/perspective.js/master/dist/perspective.min.js"></script>
<canvas id="canvas" width="400" height="300"></canvas>

HTML5 Canvas sub-pixel position rendering issue

I'm trying to draw multiple rectangles positioned very densely in a straight line so they form a single shape like so:
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const a = 87;
const l = 300;
const col = 'rgba(0,0,64)';
const q = 4; // density - change
ctx.moveTo(300, 100);
ctx.lineTo(
300 + (Math.cos(degtorad(a)) * l),
100 + (Math.sin(degtorad(a)) * l)
);
ctx.strokeStyle = col;
ctx.lineWidth = 3;
ctx.stroke();
stack(l, a, col);
function stack(length, angle, color) {
ctx.fillStyle = color;
for (let i = 1; i < length * q; i++) {
let x = 100 + (Math.cos(degtorad(angle)) * i / q);
let y = 100 + (Math.sin(degtorad(angle)) * i / q);
console.log(`x:${x}, y:${y}`);
ctx.beginPath();
ctx.rect(x, y, 150, 100);
ctx.fill();
}
}
function degtorad(angle) {
return angle * (Math.PI / 180.0);
}
https://jsfiddle.net/fallenartist/qeo7a1gx/
However, I don't like the jagged edge at all! It looks like there are issues with canvas rendering at sub-pixel coordinates. Notice how rendering of edges of absolutely positioned rectangles differ from a straight line:
Can anyone explain why is this? And how it can be improved?

WebGL 2D Canvas Point convert to 3D world point

I have a problem with my code or I expect the wrong value. What I want to achieve is to convert 2D points from canvas to 3D world space point.
If I understand correctly, I should always get the same points regardless of the rotation of the camera because I don't want a VIEW SPACE but a WORLD SPACE point. So imagine that I click on facing wall of cube this between X and Z axis, then that what I think I should get constant Y value and it is working correctly until I make some rotation change to my camera. If we will make that camera is looking on that wall but with some angle then we will be got for each click different values for Y-axis which one should be constant because each point on that wall is on the same Y position.
var r = canvas.getBoundingClientRect();
var x = clientX - r.left;
var y = height - (clientY - r.top);
var projectionMatrix = matrix4.perspective(fov , ratio, near, far);
// convert to clip space
var xClipSpace = x / width * 2.0 - 1.0;
var yClipSpace = y / height * -2.0 + 1.0;
var zClipSpace = 1;
// convert back from clip space to world space
var xyzVec3 = vector3.create(xClipSpace ,yClipSpace ,zClipSpace);
var transfrom = matrix4.multiply(projectionMatrix,viewMatrix);
var inverse = matrix4.invert(transform);
var result = vector3.transformMat4(xyzVec3,inverse);
What I doing wrong ?
I'm not 100% sure I understand your diagram but otherwise your code looks fine.
In most WebGL math a frustum goes -Z in the distance. Of course you can rotate that based on the view. But in any case if you pass clip space [x, y, -1] through the inverse of the (projection * view) matrix then you'll get some point of the far plane of the view frustum. As the view rotates that point moves with the frustum.
If we will make that camera is looking on that wall but with some angle then we will be got for each click different values for Y-axis which one should be constant because each point on that wall is on the same Y position.
No: If you rotate the camera that entire wall will rotate so points on it will rotate.
Here's a diagram looking down on top of the view frustum in world space. The view is rotating. If clipX and clipY are 0 then the point being computed is on the center of the far plane of the frustum (Z = 1 in clip space). You can see that point rotating even though it stays on the plane. Its position in view space would not change but its position in world space changes because the entire view frustum is effectively being rotated.
Of course you'd get a different value for Y if you rotate the camera.
const v3 = twgl.v3;
const m4 = twgl.m4;
const ctx = document.querySelector('canvas').getContext('2d');
const boxTop = [
[-1, 1, -1],
[-1, 1, 1],
[ 1, 1, 1],
[ 1, 1, -1],
];
function render(time) {
time *= 0.001;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
const fov = 60 * Math.PI / 180;
const ratio = ctx.canvas.clientWidth / ctx.canvas.clientHeight;
const near = 10;
const far = 40;
const projectionMatrix = m4.perspective(fov , ratio, near, far);
const viewMatrix = m4.rotationY(time);
// convert to clip space
const xClipSpace = 0;
const yClipSpace = 0;
const zClipSpace = 1;
// convert back from clip space to world space
const xyzVec3 = v3.create(xClipSpace ,yClipSpace ,zClipSpace);
const transform = m4.multiply(projectionMatrix, viewMatrix);
const inverse = m4.inverse(transform);
const result = m4.transformPoint(inverse, xyzVec3);
// -------------
ctx.setTransform(1, 0, 0, 1, 150.5, 75.5);
// draw origin
ctx.beginPath();
for (let i = -200; i <= 200; i += 20) {
ctx.moveTo(-400, i);
ctx.lineTo( 400, i);
ctx.moveTo(i, -400);
ctx.lineTo(i, 400);
}
ctx.strokeStyle = '#DDD';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-400, 0);
ctx.lineTo( 400, 0);
ctx.moveTo(0, -400);
ctx.lineTo(0, 400);
ctx.strokeStyle = '#444';
ctx.stroke();
ctx.fillStyle = '#888';
ctx.fillText('x', 140, 10);
ctx.fillText('z', 5, -65);
// draw frustum
ctx.beginPath();
for (let i = 0; i < 4; ++i) {
const v0 = m4.transformPoint(inverse, boxTop[i]);
const v1 = m4.transformPoint(inverse, boxTop[(i + 1) % 4]);
drawLine(ctx, v0, v1);
}
ctx.strokeStyle = 'black';
ctx.stroke();
{
ctx.beginPath();
ctx.arc(result[0], result[2], 3, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.fillText(`${result[0].toFixed(2)}, ${result[2].toFixed(2)}`, result[0] + 5, result[2] + 3);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function drawLine(ctx, v0, v1) {
ctx.moveTo(v0[0], v0[2]);
ctx.lineTo(v1[0], v1[2]);
}
render();
canvas { border: 1px solid black; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.js"></script>

How to add a fade effect to only certain elements on a html canvas

I have a canvas with multiple circles in different colours and I want add a fade out effect only to some circles. The effect is only applicable to the ones in red and green.
The code is as follows
function drawPiece(pieceX, pieceY, color) {
if (color === "rgba(0,0,0,1)" || color === "rgba(255,255,255,1)"){
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(pieceX, pieceY, 50, 0, 2 * Math.PI, false);
ctx.fill();
ctx.lineWidth = "4";
ctx.strokeStyle = "rgba(0,0,0,1)";
ctx.stroke();
ctx.closePath();
}
else {
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(pieceX, pieceY, 10, 0, 2 * Math.PI, false);
ctx.fill();
ctx.lineWidth = "4";
ctx.strokeStyle = "rgba(0,0,0,1)";
ctx.stroke();
ctx.closePath();
setTimeout(function(){
var fadeTarget = document.getElementById("canvasGame");
var fadeEffect = setInterval(function () {
if (!fadeTarget.style.opacity) {
fadeTarget.style.opacity = 1;
}
if (fadeTarget.style.opacity > 0) {
fadeTarget.style.opacity -= 0.02;
} else {
clearInterval(fadeEffect);
}
}, 20);
},0.5);
}
}
The fade effect works but it fades out the whole canvas and not the individual circles.
How can I achieve this, that only some elements are faded out.
Thanks in advance
A great canvas 2d resource is MDN's CanvasRenderingContext2D
Animations using canvas.
You will need a render loop if you want to animate canvas content.
The render loop is called 60 times a second, if possible, drawing too much and the rate will drop below 60fps.
The main loop clears the canvas, then draws the animated content, then requests the next frame.
requestAnimationFrame(mainLoop); // request the first frame to start the animation
function mainLoop() {
ctx.globalAlpha = 1; // default to 1 in case there is other content drawn
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); // clear the canvas
drawContent(); // render the content.
requestAnimationFrame(mainLoop); // request the next frame (in 1/60th second)
}
A function to draw the circle. You can remove the alpha from the color and use globalAlpha to set the transparency.
Math.TAU = Math.PI * 2; // set up 2 PI
function drawCircle(x, y, radius, color, alpha = 1) {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.strokeStyle = "#000";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.TAU);
ctx.fill();
ctx.stroke();
}
Create an object to hold a circle's description and an array to put them in
const circles = [];
function circle(x,y,r = 10, col = "#FFF", alpha = 1) {
return {x, y, r, col, alpha, alphaTarget: alpha};
}
Then in the drawContent function draw the circles one at a time
function drawContent() {
for (const circle of circles) {
if(circle.alpha !== circle.alphaTarget) {
const aStep = circle.alphaTarget - circle.alpha;
const dir = Math.sign(aStep);
circle.alpha += Math.min(Math.abs(aStep), dir * 0.02)) * dir;
}
drawCircle(circle.x, circle.y, circle.r, circle.col, circle.alpha);
}
}
Demo
The demo draws 100 circles each with their own color and alpha. The alpha is randomly selected to fade out and then back in.
You will need a render loop if you want to animate canvas content.
I move the circle so that if a device is to slow to render the content then it will be easier to see the low frame rate.
Math.TAU = Math.PI * 2; // set up 2 PI
Math.rand = (val) => Math.random() * val;
Math.randI = (val) => Math.random() * val | 0;
requestAnimationFrame(mainLoop);
const ctx = canvas.getContext("2d");
const W = canvas.width = innerWidth; // size canvas to page
const H = canvas.height = innerHeight; // size canvas to page
const circleCount = 100;
const circleFadeRate = 0.01; // per 60th second
const circles = [];
const circle = (x,y,r = 10, col = "#FFF", alpha = 1) => ({x, y, r, col, alpha, alphaTarget: alpha});
createCircles();
function createCircles() {
var i = circleCount;
while (i--) {
circles.push(circle(Math.rand(W), Math.rand(H), Math.rand(10) + 10, "#" + Math.randI(0xFFF).toString(16).padStart(3,"0"), 1));
}
circles.sort((a,b) => a.r - b.r); // big drawn last
}
function mainLoop() {
ctx.globalAlpha = 1;
ctx.clearRect(0, 0, W, H);
drawContent();
requestAnimationFrame(mainLoop);
}
function drawCircle(x, y, radius, color, alpha = 1) {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.TAU);
ctx.fill();
ctx.stroke();
}
function drawContent() {
for (const circle of circles) {
if(circle.alpha !== circle.alphaTarget) {
const aStep = circle.alphaTarget - circle.alpha;
const dir = Math.sign(aStep);
circle.alpha += Math.min(Math.abs(aStep), 0.02) * dir;
} else if(Math.random() < 0.01) {
circle.alphaTarget = circle.alpha < 0.7 ? 1 : Math.random() * 0.4;
}
circle.y += (circle.r - 10) / 5;
circle.y = circle.y > H + circle.r + 2 ? -(circle.r + 2) : circle.y;
drawCircle(circle.x, circle.y, circle.r, circle.col, circle.alpha);
}
}
body {
padding: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
For more information on the 2D canvas API see the link at top of this answer.
Canvas is a painting surface. Meaning you can't change it after you paint it. You can only clear it, or paint over it. Just like a real painting, you can't change the color of a stroke you've already painted.
So you must clear the canvas and then redraw it all, except this time draw some circles with a different opacity. Just change the last number on those rgba values to be between 0 and 1 to change the opacity.
Store opacity in a variable somewhere:
var circleOpacity = 1;
Change the opacity and then redraw in your interval function:
circleOpactiy -= 0.2;
drawMyCanvas();
Now draw the some pieces with a fillStyle something like:
ctx.fillStyle = shouldBeFaded ? `rgba(0,0,0,${circleOpacity})` : 'rgba(0,0,0,1)'
Alternatively, you could position two canvases absolutely so they are on top of each other and you could fade the top one as you are already doing. That way you won't have to re-render the canvas constantly. If the only thing you want to do is fade some circles, this might be easier. But if you want to anything more complex on that canvas (like render a game of some sort) you'll want to redraw the canvas every frame of animation anyway.

Is there a way to alter CanvasRenderingContext2D.arc so it shows like a pie?

I've been experimenting with the HTML Canvas for weeks, and I use .arc to make circles, but when the circle is incomplete, it doesn't show like a piece of pie. Instead, it uses the shortest distance possible from one end to the other, and it fills the rest! Is there a way for it to show up as pie pieces?
Here's an example using .arc:
<html>
<head>
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(100, 75, 50, 0, 2) //supposedly radians for 2
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
</head>
<body>
<canvas id="canvas" width="1000" height="600"></canvas>
</body>
</html>
So what happens is that .arc() literally creates a angled path from your provided x and y coordinates. What you need to do is:
move your 'drawing pointer' where you want to place the circle
make a 'path' from that position, which is the center of your circle
draw the actual pie arc
go back to your circle to close that path.
The following code illustrates a working example of this:
const WIDTH = 100;
const HEIGHT = 100;
const RADIUS = 50;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = WIDTH;
canvas.height = HEIGHT;
document.body.appendChild(canvas);
// identify center of your circle for learning purposes
// let's use the center of the canvas:
const [dx, dy] = [WIDTH / 2, HEIGHT / 2];
context.fillStyle = 'orange';
context.fillRect(dx, dy, 5, 5);
// clean / begin your paths
context.beginPath();
// move to (and start your path to) your position:
context.moveTo(dx, dy);
// create a circle using the dx,dy as the center of the circle:
const startAngleInRadians = 0;
const endAngleInRadians = Math.PI * 0.5;
const goAntiClockwise = false;
context.arc(dx, dy, RADIUS, startAngleInRadians, endAngleInRadians, goAntiClockwise);
// move back to your position (not required if you only draw 1 pie since your paths dont change):
context.moveTo(dx, dy);
// let's fill our pie with a pink color!:
context.fillStyle = '#FF00FF55';
context.fill();
To illustrate what is actually happening I made a simple canvas animation:
const WIDTH = 200;
const HEIGHT = 200;
const RADIUS = 50;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const [dx, dy] = [0, 0];
canvas.width = WIDTH;
canvas.height = HEIGHT;
document.body.appendChild(canvas);
context.strokeStyle = 'red';
function sleep(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, timeout);
});
}
async function showHowItWorks() {
// move from arc center to the arc start position:
for (let i=0; i<RADIUS; i++) {
context.beginPath();
context.moveTo(dx, dy);
context.lineTo(dx + i, dy);
context.stroke();
await sleep(50);
}
// draw the arc from start position to arc end:
const angle = Math.PI * 0.5;
for (let i=0; i<angle; i+=0.05) {
context.beginPath();
context.arc(dx, dy, RADIUS, 0, i, false);
context.stroke();
await sleep(50);
}
// move from arc end back to the arc center:
for (let i=50; i>=0; i--) {
context.moveTo(dx, dy + RADIUS);
context.lineTo(dx, dy + i);
context.stroke();
await sleep(50);
}
}
showHowItWorks();
Of course, there are some flaws: I am not sure whether the .arc() command moves the 'drawing pointer' -like .moveTo()- or draws from the position to the arc -like .lineTo()-.
In the first case, the pointer jumps from your center position to the arc. In the second case it actually creates a line from your center towards the arc. But anyway, both ways will give you the same end result in most cases.

Categories

Resources