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>
I was trying to make two different shapes that are different colors but it isn't working. Both of the shapes are the same colors. Please help!(Please note that I am not the best coder in the world)
I've looked for other examples on this website, but all of them use the lineTo() method and I would like to use the rect() method just to make things easier.
//make canvas and set it up
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
canvas.style.position = 'absolute';
canvas.style.left = '0px';
canvas.style.top = '0px';
canvas.style.backgroundColor = '#D0C6C6';
var cH = canvas.height;
var cW = canvas.width;
//draw paddles
//variables
var paddleLength = 120;
var redPaddleY = window.innerHeight / 2;
var bluePaddleY = window.innerHeight / 2;
var paddleWidth = 20;
//drawing starts
function drawPaddles() {
//RED PADDLE
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.clearRect(0, 0, cW, cH);
ctx.rect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
//BLUE PADDLE
var bluePaddle = function(color) {
ctx.fillStyle = color;
ctx.clearRect(0, 0, cW, cH);
ctx.rect(cH / 12 * 14, bluePaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
redPaddle('red');
bluePaddle('blue');
};
var interval = setInterval(drawPaddles, 25);
Whenever you add a shape to the canvas it becomes part of the current path. The current path remains open until you tell the canvas to start a new one with beginPath(). This means that when you add your second rect() it is combined with the first and filled with the same colour.
The simplest fix would be to use the fillRect() function instead of rect which begins, closes and fills a path in one call.
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.fillRect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
};
If you still want to use rect() you should tell the canvas to begin a new path for each paddle.
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.rect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
I would also suggest moving the clearRect() outside of the drawing functions too. Clear once per frame and draw both paddles.
...
ctx.clearRect(0, 0, cW, cH);
redPaddle();
bluePaddle();
...
You should also investigate requestAnimationFrame() to do your animation loop as it provides many performance improvements over intervals.
I am trying to write the description of each edge along the rectangle. The reason is to describe the length of each edge inside and outside rectangles (alongside). Is there a way I can achieve it?
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// Clip a rectangular area
ctx.rect(50, 20, 200, 120);
ctx.stroke();
ctx.clip();
// Draw red rectangle after clip()
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 150, 100);
This should show 200 above the top edge (outside) and 150 along the left edge (outside)
Using #stealththeninja's comment (pointing to this answer - text in html canvas) and this jsfiddle (for text rotation), I was able to build the code below. Hope it fits within your specs.
Screenshot of the result attached.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
const rectPosX = 50;
const rectPosY = 50;
const rectLength = 200;
const rectHeight = 150;
ctx.fillStyle = "red";
ctx.fillRect(rectPosX, rectPosY, rectLength, rectHeight);
ctx.fillStyle = "blue";
ctx.fillText('200', rectPosX + rectLength / 2, rectPosY);
ctx.fillText('150', rectPosX, rectPosY + rectHeight / 2);
ctx.fillText('200', rectPosX + rectLength / 2, rectPosY + rectHeight);
ctx.save();
ctx.translate(rectPosX + rectLength, rectPosY + rectHeight / 2);
ctx.rotate(0.5*Math.PI);
ctx.fillText('150', 0, 0);
ctx.restore();
<canvas id="myCanvas" width="400" height="300"></canvas>
I am trying to create a hole in canvas; and loading image in canvas after.
I want this canvas to contain a hole at a top layer.
I just read about counter-clockwise canvas rules and then created hole with counter clockwise position as below-
var c = document.getElementById("canvas-front");
var ctx = c.getContext("2d");
var centerX = c.width / 2;
var centerY = c.offsetTop;
var radius = 30;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI,true);//boolean true to counter-clockwise
ctx.fillStyle = 'black';
ctx.fill();
I have canvas before image in it-
After applying image in this canvas-
Canvas-
<canvas id="canvas-front" width="261" height="506"></canvas>
As you can see from both the images, I can't get this hole in canvas work.
How do I create this arc so that image doesn't overlap this.
I get this issue solved-
What I had to do-
On change of file I did empty to canvas and redrew image and then while image is being loaded
the arc is being created to counter-clockwise position-
Drawing the canvas again on image change-
var canvas = document.getElementById("canvas-front");
canvas.width = canvas.width;//blanks the canvas
var c = canvas.getContext("2d");
Creating image and giving it required src from changed input-
var img = new Image();
img.src = e.target.result;
Loading image and creating arc parallely-
img.onload = function () {
c.drawImage(img, 0, 0);
var centerX = canvas.width / 2;
var centerY = canvas.offsetTop;
var radius = 30;
c.beginPath();
c.arc(centerX, centerY, radius, 0, 2 * Math.PI, true);
c.fillStyle = 'black';
c.fill();
}
You can draw arcs and you can draw images on a canvas using javascript, but is there anyway to do both? that is, is there anyway to draw an image as the arc instead of a solid line?
If not, is there a seperate way to arc an image?
I tried doing a pixel-by-pixel transformation of the image along the points of an arc, but it ended up being very slow and looking pretty poor since I can't directly get pixel data from javascript (or can you? I didnt see a way) so for each pixel, I needed to calculate the point along the arc, draw the current image pixel, refetch it, draw the image data to the calculated point, then clear that point on the canvas.
Have you tried setting the strokeStyle to a CanvasPattern based on an image? It looks like you could do something like this (assuming img is an HTMLImageElement that you want to draw from and ctx is a CanvasRenderingContext2D):
var pattern = ctx.createPattern(img, "repeat");
ctx.strokeStyle = pattern;
ctx.beginPath();
ctx.arc(123, 408, 80, 0, 1.5*Math.PI, false);
ctx.stroke();
Use this html to create arc : -
<canvas id="myCanvas" width="578" height="250"></canvas>
<script>
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
var startAngle = .8 * Math.PI;
var endAngle = 2.2 * Math.PI;
var counterClockwise = false;
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
context.lineWidth = 30;
// line color
context.strokeStyle = 'blue';
context.stroke();
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
var startAngle = 8 * Math.PI;
var endAngle =2.3 * Math.PI;
var counterClockwise = false;
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
context.lineWidth = 30;
// line color
context.strokeStyle = 'red';
context.stroke();
</script>