I'm trying to draw this
flower using a loop in canvas. I'm confused where to start. Do i draw the petals first? Do i transform them after to get all 5 petals. This is what I have so far for a 500 x 500 canvas.
var canvas = document.getElementById('flowerCanvas');
var ctx = canvas.getContext('2d');
var radius = 20;
var petals = 5;
ctx.beginPath();
ctx.arc(250, 250, radius, 0, 2*Math.PI);
ctx.fillStyle = "black";
ctx.fill();
ctx.save();
ctx.translate(250,250);
for(var i = 0; i < 5; i++){
ctx.lineTo(200,0);
ctx.stroke();
}
ctx.restore();
Using Path2D to buffer complex paths
You can use a Path2D object to store each petal. It has the same path functionality as the 2D context so code can easily be converted to a 2D path and then rendered without the need to do all the path creation steps. If you have a lot of rendering that involves many repeated paths this will give you some additional performance.
Create path
You create the petal relative to its rotation point.
function createPetal(length, width){
const path = new Path2D();
// draw outer line
path.moveTo(0,0);
path.lineTo(length * 0.3, -width);
path.lineTo(length * 0.8, -width);
path.lineTo(length, 0);
path.lineTo(length * 0.8, width);
path.lineTo(length * 0.3, width);
// close the path so that it goes back to start
path.closePath();
// create the line down the middle.
path.moveTo(0,0);
path.lineTo(length,0);
return path;
}
Note for this to work the petals must be drawn relative to the rotation point.
Render path
You now need to draw the path several times around the circle.
// x,y is center
// count number of petals
// startAt is the angle of the first
function drawPetals(x, y, count, startAt, petal){
const step = (Math.PI * 2) / count;
ctx.setTransform(1, 0, 0, 1, x, y); // set center
ctx.rotate(startAt); // set start angle
for(var i = 0; i < count; i+= 1){
ctx.stroke(petal); // draw a petal
ctx.rotate(step); // rotate to the next
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default
}
Put it all together.
Now all you need to do is create a petal, then draw them, and add a circle at the center
// col is the stroke color
// linewidth is the thing to do with lines
// fitScale is how well to fit the space. Less than one to fit the canvas
// petalCount i will let you guess what that does.
function drawFlower(col, lineWidth,fitScale, petalCount) {
ctx.strokeStyle = col;
ctx.lineWidth = lineWidth;
const size = Math.min(ctx.canvas.width, ctx.canvas.height) * fitScale * 0.5;
drawPetals(ctx.canvas.width / 2, ctx.canvas.height / 2, 5, 0, createPetal(size, size * 0.2));
ctx.beginPath();
ctx.arc(ctx.canvas.width / 2, ctx.canvas.height / 2, size*0.15 , 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
}
The code above runs
const ctx = canvas.getContext("2d");
function createPetal(length, width) {
const path = new Path2D();
path.moveTo(0, 0);
path.lineTo(length * 0.3, -width);
path.lineTo(length * 0.8, -width);
path.lineTo(length, 0);
path.lineTo(length * 0.8, width);
path.lineTo(length * 0.3, width);
path.closePath();
path.moveTo(0, 0);
path.lineTo(length, 0);
return path;
}
// x,y is center
// count number of petals
// startAt is the angle of the first
function drawPetals(x, y, count, startAt, petal) {
const step = (Math.PI * 2) / count;
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.rotate(startAt);
for (var i = 0; i < count; i += 1) {
ctx.stroke(petal);
ctx.rotate(step);
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default
}
function drawFlower(col, lineWidth, fitScale, petalCount) {
ctx.strokeStyle = col;
ctx.lineWidth = lineWidth;
const size = Math.min(ctx.canvas.width, ctx.canvas.height) * fitScale * 0.5;
drawPetals(ctx.canvas.width / 2, ctx.canvas.height / 2, 5, 0, createPetal(size, size * 0.2));
ctx.beginPath();
ctx.arc(ctx.canvas.width / 2, ctx.canvas.height / 2, size*0.15 , 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
}
drawFlower("black",4,0.95,5);
canvas {
border: 2px solid black;
}
<canvas id="canvas" width="500" height="500"></canvas>
For browsers without Path2D
Not all browsers support Path2D. To do the same you can store the path of the petal as an array. As there are several paths and one is open and the other closed you need to add some path drawing rules. In this case is the last point is the same as the first, the path is closed.
// the array holds normalised coords that need to be scaled when drawing
const petal = [
[ [0,0],[0.3,-1],[0.8,-1],[1,0],[0.8,1],[0.3,1],[0,0] ],
[ [0,0],[1,0] ],
]
So now rather than create petal you have a function that draws a petal using a path
function drawPetal(path,width,height){
var i = 0;
do{ // loop through paths
const p = path[i];
let j = 0;
ctx.moveTo(p[j][0] * width, p[j ++][1] * height);
while(j < p.length - 1){
ctx.lineTo(p[j][0] * width, p[j ++][1] * height);
}
// is the path closed ?
if(p[j][0] === p[0][0] && p[j][1] === p[0][1]){
ctx.closePath();
}else{
ctx.lineTo(p[j][0] * width,p[j][1] * height)
}
} while(++i < path.length);
}
And the draw flower function needs to be changed to use the new drawPetal but the method is the same, draw each petal in turn rotating it using the current transform.
As code example
const ctx = canvas.getContext("2d");
const petal = [
[
[0, 0],
[0.3, -1],
[0.7, -1],
[1, 0],
[0.7, 1],
[0.3, 1],
[0, 0]
],
[
[0, 0],
[1, 0]
],
];
function drawPetal(path, width, height) {
var i = 0;
do { // loop through paths
const p = path[i];
let j = 0;
ctx.moveTo(p[j][0] * width, p[j++][1] * height);
while (j < p.length - 1) {
ctx.lineTo(p[j][0] * width, p[j++][1] * height);
}
if (p[j][0] === p[0][0] && p[j][1] === p[0][1]) { // is the path closed ?
ctx.closePath();
} else {
ctx.lineTo(p[j][0] * width, p[j][1] * height)
}
} while (++i < path.length);
}
function drawPetals(x, y, count, startAt, petal, width, height) {
const step = (Math.PI * 2) / count;
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.rotate(startAt);
for (var i = 0; i < count; i += 1) {
drawPetal(petal, width, height);
ctx.rotate(step);
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore default
}
function drawFlower(col, lineWidth, fitScale, petalCount) {
ctx.strokeStyle = col;
ctx.lineWidth = lineWidth;
const size = Math.min(ctx.canvas.width, ctx.canvas.height) * fitScale * 0.5;
ctx.beginPath();
drawPetals(ctx.canvas.width / 2, ctx.canvas.height / 2, 5, -Math.PI / 2, petal, size, size * 0.2);
ctx.stroke();
ctx.beginPath();
ctx.arc(ctx.canvas.width / 2, ctx.canvas.height / 2, size * 0.15, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
}
drawFlower("black", 4, 0.95, 5);
canvas {
border: 2px solid black;
}
<canvas id="canvas" width="500" height="500"></canvas>
Related
I am creating an inner solar sytem model from the percpective of Earth for creating astology birthcharts references. I am able to draw an optical axis line for the Earth - Sun axis, and the Earth - Moon axis but I can not for the life of me figure the trigonometry for the remaining inner planets.
Here is a working pen.
https://codepen.io/pjdroopypants/pen/wvEKeOm
window.onload = function(){
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
cw = canvas.width,
ch = canvas.height,
time = 1;
function circle(radius,color,x,y){
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(x,y,radius,0,2*Math.PI,true);
ctx.fill();
ctx.closePath();
}
function line(color,ax,ay,bx,by){
ctx.beginPath();
ctx.moveTo(ax*2,ay);
ctx.lineTo(bx+0.5,by+0.5);
ctx.strokeStyle = color;
ctx.stroke();
ctx.closePath();
}
var sunDegree = Math.floor(Math.random() * 359) + 1; //temporary, will use function from live planetary data
var merDegree = Math.floor(Math.random() * 359) + 1; //temporary, will use function from live planetary data
var venDegree = Math.floor(Math.random() * 359) + 1; //temporary, will use function from live planetary data
var marDegree = Math.floor(Math.random() * 359) + 1; //temporary, will use function from live planetary data
var mooDegree = Math.floor(Math.random() * 359) + 1; //temporary, will use function from live planetary data
var interval = 570.0930061551268;
function animate(){
ctx.save();
ctx.clearRect(0, 0, 1100, 1100);
ctx.translate(cw/2,ch/2);
//Earth
ctx.rotate(-(time / interval)+ Math.PI);
circle(15,"blue",0,0);
ctx.translate(480,0);
line("blue",-240,0,0,0);
ctx.translate(-480,0);
ctx.rotate((time / interval)+ Math.PI);
//Moon
var moontime = (time / (interval / 13.36996336996337)+mooDegree);
ctx.rotate(-moontime);
ctx.translate(23,0);
circle(3,"black",0,0);
ctx.translate(457,0);
line("#6a6a6a",-230,0,0,0);
ctx.translate(-480,0);
ctx.rotate(moontime);
//Sun
var suntime = time / interval;
ctx.rotate(-suntime);
ctx.translate(120,0);
circle(15,"yellow",0,0);
ctx.translate(360,0);
line("yellow",-182,0,0,0);
ctx.translate(-360,0);
ctx.rotate(suntime);
//Mercury
var mertime = (time / (interval / 4.150568181818182))+merDegree;
ctx.rotate(-(time / (interval / 4.150568181818182))+merDegree-suntime);
ctx.translate(40,0);
circle(15,"#898989",0,0);
ctx.translate(-40,0);
ctx.rotate((time / (interval / 4.150568181818182))+merDegree+suntime);
//Venus
ctx.rotate(-(time / (interval / 1.625500667556742))+venDegree-suntime);
ctx.translate(80,0);
circle(15,"#b9955b",0,0);
ctx.translate(-80,0);
ctx.rotate((time / (interval / 1.625500667556742))+venDegree);
//Mars
ctx.rotate(-(time / (interval / 0.5316593886462882))+marDegree);
ctx.translate(160,0);
circle(15,"#9f5e13",0,0);
ctx.translate(-160,0);
ctx.rotate((time / (interval / 0.5316593886462882))+marDegree);
ctx.restore();
time++;
window.requestAnimationFrame(animate);
}
window.requestAnimationFrame(animate);
}
I attempted to reverse rotate each animation iteration by subtracting the suns rotating angle but that just makes the axis line horizontal and not back to center.
Encapsulate using objects
Your code is a mess
You need to encapsulate the various parts so you can work with them individually.
The example below defines planets using a factory pattern to create planets as objects.
Each planet gets orbital ang, rate, and distance from the planet they orbit. Eg Mercury, Venus, Earth, Mars all orbit the Sun, and the Moon orbits the Earth.
Planets are created in orbital order. IE can not create planet before you create the planet it orbits.
On each animation frame each planet's position is calculated relative to the planet it orbits in the same order as they were created.
Before rendering the planet we want to center the view on a selected planet (in this case the Earth). The centered planets position is used to create a global matrix that will move all planets to the correct position.
Then when drawing each planet we multiply the global matrix by the planet's matrix ctx.setTransform(...globalMat); ctx.transform(this.cos, this.sin, -this.sin, this.cos, this.x, this.y); and draw the result
requestAnimationFrame(mainLoop);
const ctx = canvas.getContext("2d");
const TAU = Math.PI * 2;
const centerOnPlanetName = "Earth"; // Planet name to center view on
const globalMatrix = [1,0,0,1,0,0]; // holds position of planet to track
const solSystem = {}; // holds planets by name
const planets = []; // holds planets in order of created
const planetCommon = { // common properties of planets
x: 0, y: 0, cos: 1, sin: 0,
update(time) {
const ang = this.startAng + this.orbitalSpeed * time;
this.cos = Math.cos(ang);
this.sin = Math.sin(ang);
if (this.orbits) { // Check if orbiting something
this.x = this.orbits.x + this.cos * this.dist;
this.y = this.orbits.y + this.sin * this.dist;
}
},
draw(ctx, globalMat) {
ctx.setTransform(...globalMat);
ctx.transform(this.cos, this.sin, -this.sin, this.cos, this.x, this.y);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, TAU);
ctx.fill();
ctx.beginPath();
ctx.lineTo(this.radius * 1.5, 0);
ctx.lineTo(300, 0);
ctx.stroke();
}
};
const planet = (name, orbitsName, dist, orbitalSpeed, startAng, radius, color) => {
planets.push(solSystem[name] = {
orbits: solSystem[orbitsName], // Set planet new planet orbits
dist, // dist from orbiting body
orbitalSpeed, // in radians per time unit
startAng, // starting angle in radians
radius,
color,
...planetCommon,
});
};
planet("Sun", undefined, 0, 0.8, - Math.PI, 20, "yellow");
planet("Mercury", "Sun", 45, 2, 0, 6, "#888");
planet("Venus", "Sun", 70, 1.2, 0, 15, "#CC8");
planet("Earth", "Sun", 120, 0.8, 0, 16, "#39B");
planet("Mars", "Sun", 175, 0.4, 0, 10, "#B43");
planet("Moon", "Earth", 30, 5, 0, 3, "#444");
function centerOn(cx, cy, planet) {
globalMatrix[4] = cx - planet.x;
globalMatrix[5] = cy - planet.y;
}
function mainLoop(time) {
time /= 1000;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
planets.forEach(planet => planet.update(time));
centerOn(ctx.canvas.width * 0.5, ctx.canvas.height * 0.5, solSystem[centerOnPlanetName]);
planets.forEach(planet => planet.draw(ctx, globalMatrix));
requestAnimationFrame(mainLoop);
}
canvas {
border: 1px solid black;
}
<canvas id="canvas" width="600" height="600"></canvas>
I need to apply several matrix transformations before drawing a shape, however (if on somewhere) I use rotate() the coordinates are inverted and/or reversed and cannot continue without knowing if the matrix was previously rotated.
How can solve this problem?
Example:
<canvas width="300" height="300"></canvas>
<script>
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
ctx.fillStyle = "silver";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, canvas.height/2);
ctx.lineTo(canvas.width, canvas.height/2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(canvas.width/2, 0);
ctx.lineTo(canvas.width/2, canvas.height);
ctx.stroke();
ctx.translate(150, 150);
ctx.rotate(-90 * 0.017453292519943295);
ctx.translate(-150, -150);
// move the red rectangle 100px to the left (top-left)
// but instead is moved on y axis (right-bottom)
ctx.translate(-100, 0);
// more matrix transformations
// ....
// ....
// now finally draw the shape
ctx.fillStyle = "red";
ctx.fillRect(150, 150, 100, 50);
</script>
Can be this Translation after rotation the solution?
It looks like you aren't resetting the canvas matrix each time you make a new transformation.
The Canvas API has the save() and restore() methods. Canvas states are stored on a stack. Every time the save() method is called, the current drawing state is pushed onto the stack. A drawing state consists of transformations that have been applied along with the attributes of things like the fillStyle. When you call restore(), the previous settings are restored.
// ...
ctx.save(); // save the current canvas state
ctx.translate(150, 150);
ctx.rotate(-90 * 0.017453292519943295);
ctx.translate(-150, -150);
ctx.restore(); // restore the last saved state
// now the rectangle should move the correct direction
ctx.translate(-100, 0);
Check out this link for more information on the save and restore methods.
OK finally, i solved the problem by rotating the translation point before applying it. This function does the trick:
function helperRotatePoint(point, angle) {
let s = Math.sin(angle);
let c = Math.cos(angle);
return { x: point.x * c - point.y * s, y: point.x * s + point.y * c};
}
rotating the translation point using the inverted angle I obtain the corrected translation
helperRotatePoint(translation_point, -rotation_angle);
working code:
let canvas = document.querySelector("canvas");
// proper size on HiDPI displays
canvas.style.width = canvas.width;
canvas.style.height = canvas.height;
canvas.width = Math.floor(canvas.width * window.devicePixelRatio);
canvas.height = Math.floor(canvas.height * window.devicePixelRatio);
let ctx = canvas.getContext("2d");
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.fillStyle = "whitesmoke";
ctx.fillRect(0, 0, canvas.width, canvas.height);
class UIElement {
constructor(x, y, width, height, color) {
// PoC
this.draw_pos = {x, y};
this.draw_size = {width, height};
this.color = color;
this.rotate = 0;
this.scale = {x: 1, y: 1};
this.translate = {x: 0, y: 0};
this.skew = {x: 0, y: 0};
this.childs = [];
}
addChild(uielement) {
this.childs.push(uielement);
}
helperRotatePoint(point, angle) {
let s = Math.sin(angle);
let c = Math.cos(angle);
return {
x: point.x * c - point.y * s,
y: point.x * s + point.y * c
};
}
draw(cnvs_ctx, parent_x, parent_y) {
// backup current state
cnvs_ctx.save();
let elements_drawn = 1;// "this" UIElement
// step 1: calc absolute coordinates
let absolute_x = parent_x + this.draw_pos.x;
let absolute_y = parent_y + this.draw_pos.y;
// step 2: apply all transforms
if (this.rotate != 0) {
cnvs_ctx.translate(absolute_x, absolute_y)
cnvs_ctx.rotate(this.rotate);
cnvs_ctx.translate(-absolute_x, -absolute_y);
// rotate translate point before continue
let tmp = this.helperRotatePoint(this.translate, -this.rotate);
// apply rotated translate
cnvs_ctx.translate(tmp.x, tmp.y);
} else {
cnvs_ctx.translate(this.translate.x, this.translate.y);
}
cnvs_ctx.scale(this.scale.x, this.scale.y);
cnvs_ctx.transform(1, this.skew.y, this.skew.x, 1, 0, 0);
// step 3: self draw (aka parent element)
cnvs_ctx.fillStyle = this.color;
cnvs_ctx.fillRect(absolute_x, absolute_y, this.draw_size.width, this.draw_size.height);
// step 4: draw childs elements
for (let i = 0; i < this.childs.length ; i++) {
elements_drawn += this.childs[i].draw(
cnvs_ctx, absolute_x, absolute_y
);
}
// done, restore state
cnvs_ctx.restore();
return elements_drawn;
}
}
// spawn some ui elements
var ui_panel = new UIElement(120, 50, 240, 140, "#9b9a9e");
var ui_textlabel = new UIElement(10, 10, 130, 18, "#FFF");
var ui_image = new UIElement(165, 25, 90, 60, "#ea9e22");
var ui_textdesc = new UIElement(17, 46, 117, 56, "#ff2100");
var ui_icon = new UIElement(5, 5, 10, 10, "#800000");
ui_panel.addChild(ui_textlabel);
ui_panel.addChild(ui_image);
ui_panel.addChild(ui_textdesc);
ui_textdesc.addChild(ui_icon);
// add some matrix transformations
ui_textdesc.skew.x = -0.13;
ui_textdesc.translate.x = 13;
ui_image.rotate = -90 * 0.017453292519943295;
ui_image.translate.y = ui_image.draw_size.width;
ui_panel.rotate = 15 * 0.017453292519943295;
ui_panel.translate.x = -84;
ui_panel.translate.y = -50;
// all ui element elements
ui_panel.draw(ctx, 0, 0);
<canvas width="480" height="360"></canvas>
I'm trying to create a zoomable canvas with rectangles arranged in a grid using pixi.js. Everything works smoothly except that the grid creates heavy moire effects. My knowledge about pixijs and webgl is only very superficial but I'm suspecting that something with the antialiasing is not working as I expect it to. I'm drawing the rectangles using a 2048x2048px texture I create beforehand in separate canvas. I make it that big so I do this so I can zoom in all the way while still having a sharp rectangle. I also tried using app.renderer.generateTexture(graphics) but got a similar result.
The black rectangles are drawn using pixi.js and the red ones are drawn using SVG as a reference. There is still moire occurring in the SVG as well but it is much less. Any ideas how I can get closer to what the SVG version looks like? You can find a a working version here.
Here's the relevant code I use to setup the pixi.js application:
// PIXI SETUP
const app = new Application({
view: canvasRef,
width,
height,
transparent: true,
antialias: false,
autoDensity: true,
resolution: devicePixelRatio,
resizeTo: window
});
const particleContainer = new ParticleContainer(2500, {
scale: true,
position: true,
rotation: true,
uvs: true,
alpha: true
});
app.stage.addChild(particleContainer);
// TEXTURE
const size = 2048;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, size, size);
ctx.fill();
const texture = PIXI.Texture.from(canvas);
// RECTANGLE GRID
const size = 10;
for(let i=0; i<2500; i++) {
const particle = Sprite.from(texture);
particle.x = i % 50 * size * 1.5;
particle.y = Math.floor(i / 50) * size * 1.5;
particle.anchor.set(0);
particle.width = size;
particle.height = size;
parent.addChild(particle);
}
Don't render sub pixel detail.
The best way to maintain a grid while avoiding artifacts is to not render grid steps below the resolution of the canvas. Eg if you have zoomed out by 100 then do not draw grids less than 100 pixels.
As this can result in grid steps popping in and out you can fade grids in and out depending on the zoom level.
The example shows one way to do this. It still has some artifacts, these are hard to avoid, but this eliminates the horrid moire patterns you get when you render all the detail at every zoom level.
The grid is defined as 2D repeating patterns to reduce rendering overhead.
Also I find grid lines more problematic than grid squares (Demo has both)
This is very basic and can be adapted to any type of grid layout.
requestAnimationFrame(mainLoop);
const ctx = canvas.getContext("2d");
const size = 138;
const grids = createPatterns(size, 4, "#222", "#EEE", "#69B", "#246");
var zoom = 1;
var zoomTarget = 16;
var zoomC = 0;
var gridType = 0;
var origin = {x: ctx.canvas.width / 2, y: ctx.canvas.height / 2};
const scales = [0,0,0];
function createPatterns(size, lineWidth, color1, color2, color3, color4){
function grid(col1, col2) {
ctx.fillStyle = col1;
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = col2;
ctx.fillRect(0, 0, size, lineWidth);
ctx.fillRect(0, 0, lineWidth, size);
}
function grid2(col1, col2) {
ctx.fillStyle = col1;
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = col2;
ctx.fillRect(0, 0, size / 2, size / 2);
ctx.fillRect( size / 2, size / 2, size / 2, size / 2);
}
const patterns = [];
const ctx = Object.assign(document.createElement("canvas"), {width: size, height: size}).getContext("2d");
grid(color1, color2)
patterns[0] = ctx.createPattern(ctx.canvas, "repeat");
grid2(color3, color4)
patterns[1] = ctx.createPattern(ctx.canvas, "repeat");
return patterns;
}
function drawGrid(ctx, grid, zoom, origin, smooth = true) {
function zoomAlpha(logScale) {
const t = logScale % 3;
return t < 1 ? t % 1 : t > 2 ? 1 - (t - 2) % 1 : 1;
}
function fillScale(scale) {
ctx.setTransform(scale / 8, 0, 0, scale / 8, origin.x, origin.y);
ctx.globalAlpha = zoomAlpha(Math.log2(scale));
ctx.fill();
}
ctx.fillStyle = grid;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.imageSmoothingEnabled = smooth;
ctx.beginPath();
ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalAlpha = 1;
const l2 = Math.log2(zoom);
scales[0] = 2 ** ((l2 + 122) % 3); // zoom limit 1 / (2 ** 122) (well beyond number precision)
scales[1] = 2 ** ((l2 + 123) % 3);
scales[2] = 2 ** ((l2 + 124) % 3);
scales.sort((a,b) => a - b);
fillScale(scales[0]);
fillScale(scales[1]);
fillScale(scales[2]);
ctx.globalAlpha = 1;
}
function mainLoop() {
if (innerWidth !== ctx.canvas.width || innerHeight !== ctx.canvas.height) {
origin.x = (ctx.canvas.width = innerWidth) / 2;
origin.y = (ctx.canvas.height = innerHeight) / 2;
zoomTarget = 16;
zoom = 1;
}
zoomC += (zoomTarget - zoom) * 0.3;
zoomC *= 0.02;
zoom += zoomC;
if (gridType === 0) {
drawGrid(ctx, grids[0], zoom, origin);
} else {
drawGrid(ctx, grids[1], zoom, origin, false);
}
requestAnimationFrame(mainLoop);
}
zoomIn.addEventListener("click", () => zoomTarget *= 2);
zoomOut.addEventListener("click", () => zoomTarget *= 1/2);
toggle.addEventListener("click", () => gridType = (gridType + 1) % 2);
* { margin: 0px;}
canvas { position: absolute; top: 0px;left: 0px; }
.UI { position: absolute; top: 14px; left: 14px; }
<canvas id="canvas"></canvas>
<div class="UI">
<button id="zoomIn">Zoom In</button><button id="zoomOut">Zoom Out</button><button id="toggle">Toggle grid type</button>
</div>
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="350" height="300"
style="border:6px solid black;">
</canvas>
<script>
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.strokeStyle = 'gold';
ctx.strokeRect(20, 10, 160, 100);
</script>
</body>
</html>
Now, I want to go ahead and turn this drawn rectangle:
ctx.strokeStyle = 'gold';
ctx.strokeRect(20, 10, 160, 100);
Into a variable that I can just simply name "Rectangle" I can easily modify and call it out throughout my project. How can I do that? thank you!
You could use Path2D
Using Path2D to create paths and render them as needed is convenient and from a rendering standpoint paths are a little quicker as the sub paths do not need to be created every time you render the path.
It is best to create the sub paths around the origin (0,0) so you can easily move, rotate and scale them as needed.
Example creating some paths with different content
function createRect() {
const path = new Path2D();
path.rect(-70, -45, 140, 90); // add the sub path;
return path;
}
function createCircle() {
const path = new Path2D();
path.arc(0, 0, 50, 0, Math.PI * 2); // add the sub path;
return path;
}
function createRandLines() {
const path = new Path2D();
var i = 10;
while(i--) {
path.moveTo(Math.random() * 20 - 10, Math.random() * 20 - 10);
path.lineTo(Math.random() * 20 - 10, Math.random() * 20 - 10);
}
return path;
}
To create the paths
const myCircle = createCircle();
const myRect = createCircle();
const myLines1 = createRandLines();
const myLines2 = createRandLines();
You can then render any of the paths with a single function.
function strokePath(path, x, y, lineWidth = ctx.lineWidth, color = ctx.strokeStyle) { // defaults to current style
ctx.setTransform(1, 0, 0, 1, x, y); // position the path so its (0,0) origin is at x,y
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.stroke(path);
}
Passing the path position style and line width to draw the path.
const W = ctx.canvas.width;
const H = ctx.canvas.height;
strokePath(myCircle, Math.random() * W, Math.random() * H);
strokePath(myRect, Math.random() * W, Math.random() * H);
strokePath(myLines1, Math.random() * W, Math.random() * H);
strokePath(myLines2, Math.random() * W, Math.random() * H);
Example
A more detailed draw function and some organisation in regards to the create path functions.
The example creates 4 paths once and then draws them many times, randomly positioned, rotated, scaled, alpha faded, colored, and filled.
const W = canvas.width;
const H = canvas.height;
const ctx = canvas.getContext("2d");
ctx.lineCap = ctx.lineJoin = "round";
// Some math utils
Math.TAU = Math.PI * 2;
Math.rand = (m = 0, M = 1) => Math.random() * (M - m) + m;
Math.randItem = array => array[Math.random() * array.length | 0];
const FACE = [[-3,-38,-34,-32,-47,-16,-48,15,-36,34,-5,43,32,38,47,12,47,-21,25,-35],[-31,-19,-42,-6,-32,1,-9,-6,-6,-24],[5,-24,3,-6,29,2,40,-5,33,-19],[-30,15,-14,32,12,31,29,15,15,15,-2,23,-17,16],[-28,-14,-29,-6,-18,-9,-17,-15],[11,-17,12,-8,20,-6,22,-13,18,-16],[2,-39,0,-53,-9,-60],[-14,17,-16,26,-7,28,-5,22],[2,21,1,28,11,27,13,16]];
// Object to hold path types
const paths = {
rect() {
const path = new Path2D();
path.rect(-20, -10, 40, 20); // add the sub path;
return path;
},
ellipse() {
const path = new Path2D();
path.ellipse(0, 0, 20, 10, 0, 0, Math.TAU); // add the sub path;
return path;
},
randLines() {
const path = new Path2D();
var i = 10;
while (i--) {
path.moveTo(Math.rand(-20, 20), Math.rand(-20, 20));
path.lineTo(Math.rand(-20, 20), Math.rand(-20, 20));
}
return path;
},
face() {
const path = new Path2D();
FACE .forEach(sub => { // each sub path
let i = 0;
path.moveTo(sub[i++] / 3, sub[i++] / 3);
while (i < sub.length) { path.lineTo(sub[i++] / 3, sub[i++] / 3) }
path.closePath();
});
return path;
}
};
// Function to draw scaled, rotated, faded, linewidth, colored path
function strokePath(path, x, y, scale, rotate, alpha, lineWidth, color, fillColor) {
ctx.lineWidth = lineWidth * (1 / scale); //Scale line width by inverse scale to ensure the pixel size is constant
ctx.setTransform(scale, 0, 0, scale, x, y); // position the path so its (0,0) origin is at x,y
ctx.rotate(rotate);
if (fillColor) {
ctx.globalAlpha = 1;
ctx.fillStyle = fillColor;
ctx.fill(path, "evenodd");
}
ctx.globalAlpha = alpha;
ctx.strokeStyle = color;
ctx.stroke(path);
}
// create some paths and colors
const pathArray = [paths.ellipse(), paths.rect(), paths.randLines(), paths.face()];
const colors = "#F00,#FA0,#0B0,#0AF,#00F,#F0A,#000,#888".split(",");
drawRandomPath();
function drawRandomPath() {
strokePath(
Math.randItem(pathArray), // random path
Math.rand(0, W), Math.rand(0, H), // random pos
Math.rand(0.25, 1), // random scale
Math.rand(0, Math.TAU), // random rotate
Math.rand(0.5, 1), // random alpha
1, // constant lineWidth
Math.randItem(colors), // random color
Math.rand() < 0.2 ? "#EED" : undefined, // Fill 1 in 5 with white
);
setTimeout(drawRandomPath, 250); // draw another path every quarter second.
}
* {margin:0px}
canvas {border:1px solid}
<canvas id="canvas" width="600" height="190"></canvas>
You can not do that with current standards unfortunately, you will have to redraw the shape, you can do something like:
var shape = x:10,y:20,width:20,height:40
clear the canvas and redraw with created variable:
shape.width = 100;
ctx.rect(shape.x,shape.y,shape.width,shape.height);
I'm having trouble with a .rect().
I've created a grid using .drawImage(canvas...) and whenever I .stroke(), it appears again. How can I completely delete that rectangle?
Regards
Working jsFiddle
var canvas = document.getElementById('board');
var context = canvas.getContext('2d'),
wt = canvas.width,
ht = canvas.height;
canvas.onmousedown = function (e) {
e.preventDefault(); // disabling selecting
context.strokeStyle = "red";
context.lineWidth = 1;
context.rect(wt / 2 - 50, ht / 2 - 100, 100, 200);
context.fillText("<- why is it here? didn't \"clearRect()\" delete it?", 8, 8);
context.stroke();
};
function grid() {
var h = 2.5,
p = 2.5;
context.rect(0.5, 0.5, 5, 5);
context.strokeStyle = "#f0f0f0";
context.stroke(); // creating a 5x5 small rectangle in top left corner
for (i = 0; i < wt; i += p) {
p *= 2;
context.drawImage(canvas, p, 0); // replicating it horizontally...
}
for (i = 0; i < ht; i += h) {
h *= 2;
context.drawImage(canvas, 0, h); // ... and vertically
}
context.clearRect(0, 0, 5, 5); // here I am deleting that stroke, because I don't need it anymore
context.drawImage(canvas, 0, 55, 5.5, 5.5, 0, 0, 5.5, 5.5); // drawing it with drawImage();
}
grid();
clearRect clears a portion of the canvas, not a portion of the path you're drawing with rect. Call beginPath() to clear the previous rect from being included when the stroke is applied.
See this updated example.