Invert paths on canvas - javascript

Take a look following svg. The paths there are almost the same, but the second one is inverted by using evenodd filling and adding a full rectangle to the shapes inside of it.
body {
background: linear-gradient(to bottom, blue, red);
}
svg {
height: 12em;
border: 1px solid white;
}
svg + svg {
margin-left: 3em;
}
<svg viewBox="0 0 10 10">
<path d="
M 1 1 L 2 3 L 3 2 Z
M 9 9 L 8 7 L 7 8 Z
" />
</svg>
<svg viewBox="0 0 10 10">
<path fill-rule="evenodd" d="
M 0 0 h 10 v 10 h -10 z
M 1 1 L 2 3 L 3 2 Z
M 9 9 L 8 7 L 7 8 Z
" />
</svg>
Now I want to draw the same picture on the canvas. There are no problems with the first image:
~function () {
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var h = canvas.clientHeight, w = canvas.clientWidth;
canvas.height = h;
canvas.width = w;
ctx.scale(h / 10, w / 10);
ctx.beginPath();
ctx.moveTo(1, 1);
ctx.lineTo(2, 3);
ctx.lineTo(3, 2);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(9, 9);
ctx.lineTo(8, 7);
ctx.lineTo(7, 8);
ctx.closePath();
ctx.fill();
}()
body {
background: linear-gradient(to bottom, blue, red);
}
canvas {
height: 12em;
border: 1px solid white;
}
<canvas height="10" width="10"></canvas>
But how can I draw the second if I need canvas to have transparent background?
Each path fragment consists only from lines L, start from M and end by Z.
Fragments don't overlap.

The best way to create an inverse of a image is draw over the original with the globalCompositeOperation = "destination-out"
The problem with the fill rules is that many times the method used to create a shape does not match the visual representation of the image it generates.
The next snippet shows such a case. The star is quickly rendered by just crossing path lines. The nonzero fill rule creates the shape we want. But if we attempt to invert it by defining a path around it, it fails, if we use the evenodd rule it also fails showing the overlapping areas. Additionally adding an outside box adds to the strokes as well as the fills further complicating the image and the amount of work that is needed to get what we want.
const ctx = canvas.getContext("2d");
const w = (canvas.width = innerWidth)*0.5;
const h = (canvas.height = innerHeight)*0.5;
// when there is a fresh context you dont need to call beginPath
// when defining a new path (after beginPath or a fresh ctx) you
// dont need to use moveTo the path will start at the first point
// you define
for(var i = 0; i < 14; i ++){
var ang = i * Math.PI * (10/14);
var x = Math.cos(ang) * w * 0.7 + w;
var y = Math.sin(ang) * h * 0.7 + h;
ctx.lineTo(x,y);
}
ctx.closePath();
ctx.lineWidth = 5;
ctx.lineJoin = "round";
ctx.stroke();
ctx.fillStyle = "red";
ctx.fill();
canvas.onclick = ()=>{
ctx.rect(0,0,innerWidth,innerHeight);
ctx.fillStyle = "blue";
ctx.fill();
info.textContent = "Result did not invert using nonzero fill rule";
info1.textContent = "Click to see using evenodd fill";
info1.className = info.className = "whiteText";
canvas.onclick = ()=>{
info.textContent = "Inverse image not the image wanted";
info1.textContent = "Click to show strokes";
info.className = info1.className = "blackText";
ctx.fillStyle = "yellow";
ctx.fill("evenodd");
canvas.onclick = ()=>{
info.textContent = "Strokes on boundary encroch on the image";
info1.textContent = "See next snippet using composite operations";
ctx.stroke();
ctx.lineWidth = 10;
ctx.lineJoin = "round";
ctx.strokeStyle = "Green";
ctx.stroke();
}
}
}
body {
font-family : "arial";
}
.whiteText { color : white }
.blackText { color : black }
canvas {
position : absolute;
top : 0px;
left : 0px;
z-index : -10;
}
<canvas id=canvas></canvas>
<div id="info">The shape we want to invert</div>
<div id="info1">Click to show result of attempting to invert</div>
To draw the inverse of a shape, first fill all the pixels with the opaque value (black in this case). Then define the shape as you would normally do. No need to add extra path points.
Before you call fill or stroke set the composite operation to "destination-out" which means remove pixels from the destination wherever you render pixels. Then just call the fill and stroke functions as normal.
Once done you restore the default composite operation with
ctx.globalCompositeOperation = "source-over";
See next example.
const ctx = canvas.getContext("2d");
const w = (canvas.width = innerWidth)*0.5;
const h = (canvas.height = innerHeight)*0.5;
// first create the mask
ctx.fillRect(10,10,innerWidth-20,innerHeight-20);
// then create the path for the shape we want inverted
for(var i = 0; i < 14; i ++){
var ang = i * Math.PI * (10/14);
var x = Math.cos(ang) * w * 0.7 + w;
var y = Math.sin(ang) * h * 0.7 + h;
ctx.lineTo(x,y);
}
ctx.closePath();
ctx.lineWidth = 5;
ctx.lineJoin = "round";
// now remove pixels where the shape is defined
// both for the stoke and the fill
ctx.globalCompositeOperation = "destination-out";
ctx.stroke();
ctx.fillStyle = "red";
ctx.fill();
canvas {
position : absolute;
top : 0px;
left : 0px;
z-index : -10;
background: linear-gradient(to bottom, #6CF, #3A6, #4FA);
}
<canvas id=canvas></canvas>

ctx.fill(fillrule) also accepts "evenodd" fillrule parameter, but in this case it is not even needed since your triangles entirely overlap with your rectangle.
~function () {
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var h = canvas.clientHeight, w = canvas.clientWidth;
canvas.height = h;
canvas.width = w;
ctx.scale(h / 10, w / 10);
ctx.beginPath(); // start our Path declaration
ctx.moveTo(1, 1);
ctx.lineTo(2, 3);
ctx.lineTo(3, 2);
// Actually closePath is generally only needed for stroke()
ctx.closePath(); // lineTo(1,1)
ctx.moveTo(9, 9);
ctx.lineTo(8, 7);
ctx.lineTo(7, 8);
ctx.closePath(); // lineTo(9,9)
ctx.rect(0,0,10,10) // the rectangle
ctx.fill();
}()
body {
background: linear-gradient(to bottom, blue, red);
}
canvas {
height: 12em;
border: 1px solid white;
}
<canvas height="10" width="10"></canvas>
It would have been useful if e.g you had your triangles overlapping with an other segment of the path (here an arc):
var canvas = document.querySelectorAll('canvas');
var h = canvas[0].clientHeight, w = canvas[0].clientWidth;
drawShape(canvas[0].getContext('2d'), 'nonzero');
drawShape(canvas[1].getContext('2d'), 'evenodd');
function drawShape(ctx, fillrule) {
ctx.canvas.height = h;
ctx.canvas.width = w;
ctx.scale(h / 10, w / 10);
ctx.beginPath(); // start our Path declaration
ctx.moveTo(1, 1);
ctx.lineTo(2, 3);
ctx.lineTo(3, 2);
// here closePath is useful
ctx.closePath(); // lineTo(1,1)
ctx.arc(5,5,5,0,Math.PI*2)
ctx.moveTo(9, 9);
ctx.lineTo(8, 7);
ctx.lineTo(7, 8);
ctx.closePath(); // lineTo(9,9)
ctx.rect(0,0,10,10) // the rectangle
ctx.fill(fillrule);
ctx.fillStyle = 'white';
ctx.setTransform(1,0,0,1,0,0);
ctx.fillText(fillrule, 5, 12)
}
body {
background: linear-gradient(to bottom, blue, red);
}
canvas {
height: 12em;
border: 1px solid white;
}
<canvas height="10" width="10"></canvas>
<canvas height="10" width="10"></canvas>

Sovled it:
Use only one pair of beginPath and fill.
Replace closePath by manual lineTo to corresponding point.
And it would give you an inverted image:
~function () {
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var h = canvas.clientHeight, w = canvas.clientWidth;
canvas.height = h;
canvas.width = w;
ctx.scale(h / 10, w / 10);
ctx.beginPath(); // begin it once
ctx.moveTo(0, 0); // Add full rectangle
ctx.lineTo(10, 0);
ctx.lineTo(10, 10);
ctx.lineTo(0, 10);
ctx.moveTo(1, 1);
ctx.lineTo(2, 3);
ctx.lineTo(3, 2);
ctx.lineTo(1, 1); // not ctx.closePath();
ctx.moveTo(9, 9);
ctx.lineTo(8, 7);
ctx.lineTo(7, 8);
ctx.lineTo(9, 9);
ctx.fill(); // And fill in the end
}()
body {
background: linear-gradient(to bottom, blue, red);
}
canvas {
height: 12em;
border: 1px solid white;
}
<canvas height="10" width="10"></canvas>

Related

The act of deleting a certain line from a canvas without affecting arc 😑

I want to clear the canvas with condition on each iteration, may be performance. And, the condition is to only want to clear the line and not the arc.
I already tried chatGPT, save and restore method in JS for save the previous canvas, but it didn't work for me.
This line of code uses the clearRect method of the 2D rendering context to clear the canvas by specifying the x, y, width, and height. But, its clear the whole canvas which I don't want as I mention earlier.
For the better context of my question, you can see this image.
Any answer will be appreciated.
const canvas = document.getElementById('cvs')
const ctx = canvas.getContext('2d')
const CH = (canvas.height = 400)
const CW = (canvas.width = 400)
canvas.style.background = 'black'
const vs = () => {
let radius = 50;
let i = 0;
setInterval(() => {
let x = Math.cos(i) * radius + CH / 2;
let y = Math.sin(i) * radius + CW / 2;
if(i>2*Math.PI)return clearInterval();
/* if I add `ctx.clearRect(0, 0, CW, CH);` it's clear the whole canvas, I don't want that type of behavior */
ctx.beginPath();
ctx.arc(x,y,1,0,2*Math.PI)
ctx.closePath();
ctx.strokeStyle = "white";
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(x, y);
ctx.closePath();
ctx.strokeStyle = "red";
ctx.stroke();
i += 0.01;
}, 10)
}
vs();
body{
display:grid;
place-items:center;
min-height:100vh;
background:gray;
}
<canvas id="cvs"></canvas>
I already tried chatGPT, save and restore method in JS for save the previous canvas, but it didn't work for me.
Those store-restore the context state, color in use and the like. If you look into the box in the top-right, coincidentally you will see a link "Temporary policy: ChatGPT is banned" - that has a reason.
What you need is storing-restoring the bitmap data, getImageData() and putImageData() are the methods for that. If you're concerned about performance (though it feels a bit early), the arc() call can be skipped too, as with this step-size you won't end up with a dotted circle (of course the strokeRect() I'm using instead could be then replaced with direct pixel manipulation, but a cool thing is that it provides anti-aliasing):
const canvas = document.getElementById('cvs')
const ctx = canvas.getContext('2d')
const CH = (canvas.height = 400)
const CW = (canvas.width = 400)
canvas.style.background = 'black'
const vs = () => {
let radius = 50;
let i = 0;
let backup = false;
setInterval(() => {
let x = Math.cos(i) * radius + CH / 2;
let y = Math.sin(i) * radius + CW / 2;
if (backup) ctx.putImageData(backup, 0, 0);
if (i > 2 * Math.PI) return clearInterval();
ctx.strokeStyle = "white";
ctx.strokeRect(x, y, 2, 2);
/* ctx.beginPath();
ctx.arc(x,y,1,0,2*Math.PI)
ctx.closePath();
ctx.strokeStyle = "white";
ctx.stroke();*/
backup = ctx.getImageData(0, 0, x + 2, y + 2); // 0 or 1 will have some red pixels
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(x, y);
ctx.closePath();
ctx.strokeStyle = "red";
ctx.stroke();
i += 0.01;
}, 10)
}
vs();
body {
display: grid;
place-items: center;
min-height: 100vh;
background: gray;
}
<canvas id="cvs"></canvas>

How to use clearRect to not draw an moving object on canvas

I have a blue circle which is rotating around the red circle and moves on canvas continuously in one direction as long as the button is pressed.
Now I want to draw with the red circle while it is moving when the button is pressed (trace of its path).
Problems:
I have tried to make changes to clearRect() but I didn't succeed. the blue circle starts to draw on the canvas while moving which I don't need.
If its not possible to do with clearRect() function, Is it possible to do this by stacking canvas layers. Please help with example
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let positionX = 100;
let positionY = 100;
let X = 50;
let Y = 50;
let angle = 0;
let mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function circle(){
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(X, Y, 20, 0, Math.PI*2);
ctx.closePath();
ctx.fill();
}
function direction(){
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(positionX + X, positionY + Y, 10, 0, Math.PI*2);
ctx.closePath();
positionX = 35 * Math.sin(angle);
positionY = 35 * Math.cos(angle);
ctx.fill();
}
function animate(){
if (mouseButtonDown) {
X += positionX / 10;
Y += positionY / 10;
} else {
angle += 0.1;
}
ctx.clearRect(X-positionX,Y-positionY, 20, 20);
circle();
direction();
requestAnimationFrame(animate);
}
animate();
#canvas1{
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas1"></canvas>
<script src="script.js"></script>
</body>
</html>
Don`t stack canvas on the page
Each canvas you add to the page increases the amount of work the GPU and page compositor needs to do to render the page.
Use a second canvas that is not on the page and do the compositing by rendering the canvas to the onpage canvas using ctx.drawImage(secondCanvas, 0, 0).
This reduces the workload for the compositor, and in many cases avoid the need to do an addition image render (composite) for the second canvas I.E. onpage can require 3 drawImages (one for each canvas and once for the result) rather than 2 (once in your code and once as the result) if you use only one onpage canvas.
Using second canvas
Create a second canvas to store the drawn red lines.
You can create a copy of a canvas using
function copyCanvas(canvas, copyContent = false) {
const can = Object.assign(document.createElement("canvas"), {
width: canvas.width, height: canvas.height
});
can.ctx = can.getContext("2d");
copyContent && can.ctx.drawImage(canvas, 0, 0);
return can;
}
When you create render functions like circle, and direction pass as an argument the 2D context eg circle(ctx) so that it is easy to direct the rendering to any canvas.
function circle(ctx){
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(X, Y, redSize, 0, Math.PI*2);
ctx.fill();
}
// the background canvas
const bgCan = copyCanvas(canvas);
circle(bgCan.ctx); // will draw to the background canvas
Updating animation
When animating is is easiest to clear the whole canvas rather than mess about clearing only rendered pixels. Clearing rendered pixels gets complicated very quickly and will end up being many times slower than clearing the whole canvas.
After you clear the canvas draw the background canvas to the main canvas
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(bgCan, 0, 0);
When the mouse button is down draw the circle to the background canvas and while it is up draw to the main canvas.
Example
Adds a function to copy a canvas. copyCanvas
Clears the main canvas, and draws the background canvas onto the main canvas.
Render functions circle and direction have argument ctx to direct rendering to any context.
When mouse is down circle is drawn to background canvas bgCan else to the main canvas.
requestAnimationFrame(animate);
const ctx = canvas1.getContext('2d');
canvas1.width = innerWidth;
canvas1.height = innerHeight;
const bgCan = copyCanvas(canvas1);
const redSize = 10, blueSize = 5; // circle sizes on pixels
const drawSpeed = 2; // when button down draw speed in pixels per frame
var X = 50, Y = 50;
var angle = 0;
var mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function copyCanvas(canvas) {
const can = Object.assign(document.createElement("canvas"), {
width: canvas.width, height: canvas.height
});
can.ctx = can.getContext("2d");
return can;
}
function circle(ctx){
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(X, Y, redSize, 0, Math.PI*2);
ctx.fill();
}
function direction(ctx){
const d = blueSize + redSize + 5;
ctx.fillStyle = 'blue';
ctx.beginPath();
ctx.arc(d * Math.sin(angle) + X, d * Math.cos(angle) + Y, blueSize, 0, Math.PI*2);
ctx.fill();
}
function animate(){
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(bgCan, 0, 0);
if (mouseButtonDown) {
circle(bgCan.ctx);
X += Math.sin(angle) * drawSpeed;
Y += Math.cos(angle) * drawSpeed;
} else {
angle += 0.1;
circle(ctx);
}
direction(ctx);
requestAnimationFrame(animate);
}
#canvas1{
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
<canvas id="canvas1"></canvas>
BTW ctx.closePath() is like ctx.lineTo it is not the opposite to ctx.beginPath. A full arc or if you are just filling a shape you don't need to use ctx.closePath
BTW window is the default this, you don't need to include it, you dont use it to get at window.documentso why use it forwindow.innerWidth(same asinnerWidth` )
You could alter your code to keep track of the path of the red circle, with an array property, like this:
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
function drawCircle({x, y, radius, color}) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2);
ctx.fill();
}
const red = { x: 50, y: 50, radius: 20, color: "red", path: [] };
const blue = { x: 100, y: 100, radius: 10, color: "blue", angle: 0 };
function animate(){
if (mouseButtonDown) {
red.path.push({x: red.x, y: red.y}); // store the old value
red.x += (blue.x - red.x) / 10;
red.y += (blue.y - red.y) / 10;
} else {
blue.angle += 0.1;
}
blue.x = red.x + 35 * Math.sin(blue.angle);
blue.y = red.y + 35 * Math.cos(blue.angle);
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the whole canvas
for (const {x, y} of red.path) { // draw circle at all the previous positions
drawCircle({...red, x, y});
}
drawCircle(red);
drawCircle(blue);
requestAnimationFrame(animate);
}
animate();
Using 2 canvases also works and may perform better especially when the path of the red circle has gotten long, because the background canvas doesn't need to be cleared and redrawn. Add a 2nd canvas in your html page with the same positioning, and give them ids 'background' and 'foreground'. You can then adjust the code to draw the blue circle to the foreground and red circles to the background (or vice versa).
// Create 2 canvases, set them to full size and get the contexts
const backgroundCanvas = document.getElementById('background');
const foregroundCanvas = document.getElementById('foreground');
const background = backgroundCanvas.getContext("2d");
const foreground = foregroundCanvas.getContext("2d");
backgroundCanvas.width = innerWidth;
backgroundCanvas.height = innerHeight;
foregroundCanvas.width = innerWidth;
foregroundCanvas.height = innerHeight;
let mouseButtonDown = false;
document.addEventListener('mousedown', () => mouseButtonDown = true);
document.addEventListener('mouseup', () => mouseButtonDown = false);
// Create objects to represent the current properties of the red and blue circle
const red = { x: 50, y: 50, radius: 20, color: "red" };
const blue = { x: 100, y: 100, radius: 10, color: "blue", angle: 0};
function drawCircle(ctx, {x, y, radius, color}) {
//--- Draw a circle to the specified canvas context, ctx = foreground or background
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2);
ctx.closePath();
ctx.fill();
}
function animate(){
if (mouseButtonDown) {
red.x += (blue.x - red.x) / 10;
red.y += (blue.y - red.y) / 10;
} else {
blue.angle += 0.1;
}
blue.x = red.x + 35 * Math.sin(blue.angle);
blue.y = red.y + 35 * Math.cos(blue.angle);
drawCircle(background, red); // Draw the red circle in the background (without clearing the existing circles)
foreground.clearRect(0, 0, foregroundCanvas.width, foregroundCanvas.height); // Clear the foreground
drawCircle(foreground, blue); // Draw the blue circle on the foreground
requestAnimationFrame(animate);
}
animate();
Either way, it's convenient to abstract out the circle drawing code into a function or method, and to store the properties of the two circles in objects.
As #Blindman67's answer notes, there may be a performance cost of stacking 2 canvases, and if that is an issue you may want to try drawing the background offscreen then copying it to the on-screen canvas.
If you're not opposed to just building a particle class you can do it using them. In the snippet below I have a Circle class and a Particles class to creat what you are trying to achieve. I currently have the particles max at 500 but you can change it or delete that line all together if you ne er want them gone.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let mouseButtonDown = false;
//the array holding particles
let particles = [];
//the counter is only needed it you want to slow down how fast particles are being pushed and dispolayed
let counter = 0;
document.addEventListener("mousedown", () => (mouseButtonDown = true));
document.addEventListener("mouseup", () => (mouseButtonDown = false));
//ES6 constructor class
class Circle {
//sets the basic structor of the object
constructor(r, c) {
this.x = 100;
this.y = 100;
this.x2 = 50;
this.y2 = 50;
this.r = r; //will be assigned the argument passed in through the constructor by each instance created later
this.color = c; //same as above. This allows each instance to have different parameters.
this.angle = 0;
}
//this function creates the red circle
drawRed() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
//this function creates the blue circle
drawBlue() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x + this.x2, this.y + this.y2, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
//this function is where we'll place parameter that change our object
update() {
//makes the blue circle rotate
this.x2 = 35 * Math.sin(this.angle);
this.y2 = 35 * Math.cos(this.angle);
//mouse action is same as your code
if (mouseButtonDown) {
this.x += this.x2 / 20;
this.y += this.y2 / 20;
} else {
this.angle += 0.1;
}
}
}
//When using this type of constructor class you have to create an instance of it by calling new Object. You can create as money as you want.
let blueCircle = new Circle(10, "blue"); //passing in the radius and color in to the constructor
let redCircle = new Circle(20, "red");
//another class for the particles
class Particles {
constructor() {
this.x = redCircle.x;
this.y = redCircle.y;
this.r = redCircle.r;
this.color = redCircle.color;
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
//just wrapping all of the particle stuff into one function
function handleParticles() {
//while the mouse is held it will push particles
if (mouseButtonDown) {
particles.push(new Particles());
}
//this loops through the array and calls the draw() function for each particle
for (let i = 0; i < particles.length; i++) {
particles[i].draw();
}
//this keeps the array from getting too big.
if (particles.length > 500) {
particles.shift();
}
}
//wrap all functions into this one animate one and call requeatAnimationFrame
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
handleParticles();
//These must be called for each instance created of the object
blueCircle.drawBlue();
blueCircle.update();
redCircle.drawRed();
redCircle.update();
requestAnimationFrame(animate);
}
animate();
#canvas1{
position: absolute;
top:0;
left: 0;
width: 100%;
height: 100%;
}
<canvas id="canvas"></canvas>
I'd also like to add you can change the rate that the particles are drawn by adding a counter variable and then limiting the draw like counter % 10 == 0
EXAMPLE
add global variable let counter = 0;
then in the handleParticles function add this
function handleParticles() {
counter++
if (mouseButtonDown && counter % 10 == 0) {
particles.push(new Particles());
}
for (let i = 0; i < particles.length; i++) {
particles[i].draw();
}
if (particles.length > 500) {
particles.shift();
}
}

How can I create a perspective effect within a canvas?

I know how to create a perspective effect in vanilla CSS, but how can I create this effect in a canvas?
.scene {
width: 200px;
height: 200px;
border: 2px solid black;
margin: 40px;
}
.panel {
width: 100%;
height: 100%;
background: red;
/* perspective function in transform property */
transform: perspective(600px) rotateY(45deg);
}
<div class="scene">
<div class="panel"></div>
</div>
I tried the setTransform() method without sucess.
function drawScene(margin, size) {
ctx.strokeStyle = "black";
ctx.strokeRect(margin, margin, size, size);
}
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var margin = 100;
var size = 200;
drawScene(margin, size)
ctx.setTransform(1, -0.1, 0, 1, -10, 0);
//ctx.rotate(1 * Math.PI / 180); how to rotateY
ctx.fillStyle = "red";
ctx.fillRect(margin, margin, size, size);
ctx.fillStyle = 'black';
ctx.font = "48px Courier";
ctx.fillText("hello", margin, size);
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #d3d3d3;"></canvas>
I tried both solution from HTML Canvas: Rotate the Image 3D effect but none nailed it. The perspective effet isnt here.
You need create a path by points matrix (x1, y1; x2, y2; x3, y3; x4, y4). Apply a transform matrix in your point matrix. After, print on canvas with path.
1: You need study by tranformation matrix (http://docdingle.com/teaching/cs545/presents/p12b_cs545_WarpsP2.pdf)
2:In Summary you have a transformation' matrix to translading, space, rotate or apply perspective deformation:
//this is a tranformation matrix to percpective deformation A and B are values that apply deformation
matrix_tranf = [ [1, 0 ,0] , [0,1,0] , [ A, B, 1] ];
In my code I left any examples
for(let i = 0; i < 4; i++){
//u
x1 = matrix[i][0]
//v
y1 = matrix[i][1]
w = matrix_tranf[2][0] * x1 + matrix_tranf[2][1] * y1 + 1;
v = [ ( x1 / w ) , ( y1 / w ) ];
matrix[i] = v;
}
Look
https://codepen.io/Luis4raujo/pen/wvoqEwg
If this answer help you, check as correct or voteup!

Draw dashed and dotted rectangles on canvas in the same way css border works: draw 4 same edges

My use-case is to mimic css border rendering. Is it possible, using the CanvasRenderingContext2D::rect method with CanvasRenderingContext2D::setLineDash to simulate same border drawing as css renderer does, like border: 5px dashed red. Consider this example:
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
ctx.lineWidth = 5
ctx.strokeStyle = 'red'
ctx.lineCap = 'square'
ctx.setLineDash([10, 10]);
ctx.beginPath();
ctx.moveTo(2.5,2.5);
ctx.rect(2.5, 2.5, 195, 65);
ctx.stroke();
div {
border: 5px dashed red;
width: 200px;
height: 70px;
box-sizing: border-box;
margin-bottom: 5px;
}
canvas {
display: block;
width: 200px;
height: 70px;
}
<div></div>
<canvas width=200 height=70></canvas>
You may notice the problem is on edges.
I was trying to modify the gaps and dash sizes, but it seems impossible to get the same behaviour as in the css example: the lines on edges are bigger then the lines on the sides. As a workaround I can imagine to draw every side with a line, but I would like to use the rect method to draw in one stroke.
Thank you in advance.
CSS border-style: dashed algorithm is not tied by specs, so it will be impossible to render exactly the same in the canvas API.
Then, you've got to know that even CSS renders it line by line: border is a shorthand for all the border-top-XXX, border-right-XXX, border-bottom-XXX, border-left-XXX.
And that's why it behaves like that : each border has its line-dash set independently of the others.
Anyway, if you want to do it with the canvas API, the easiest solution is to do the same, using four lines, and setting their line-dash separately.
Here is a rough attempt at normalizing the dashes in order to get them always start and end at edges:
var ctx = c.getContext('2d');
ctx.lineCap = 'square';
// returns a normalized dashArray per segment
// This in no way does the same as any browser's implementation,
// this is just a lazy way to always get dashes start and end at edges
function getLineDash(x1, y1, x2, y2) {
var length = Math.hypot((x2 - x1), (y2 - y1));
var dash_length = length / 8;
var nb_of_dashes = length / dash_length;
var dash_gap = (dash_length * 0.66);
dash_length -= dash_gap * 0.33;
return [dash_length, dash_gap];
}
function draw() {
ctx.lineWidth = lineWidth_.value;
ctx.clearRect(0, 0, c.width, c.height);
var points = [
[x1_.value, y1_.value],
[x2_.value, y2_.value],
[x3_.value, y3_.value],
[x4_.value, y4_.value]
];
points.forEach(function(pt, i) {
var next = points[(i + 1) % points.length];
ctx.beginPath();
ctx.moveTo(pt[0], pt[1]);
ctx.lineTo(next[0], next[1]);
ctx.setLineDash(getLineDash(pt[0], pt[1], next[0], next[1]));
ctx.stroke();
});
}
draw();
document.oninput = function(e) {
if (e.target.parentNode.parentNode === inputs_) {
draw();
}
}
label {
display: inline-block;
}
input {
max-width: 50px;
}
<div id="inputs_">
<label>x1<input type="number" id="x1_" value="10"></label>
<label>y1<input type="number" id="y1_" value="25"></label>
<label>x2<input type="number" id="x2_" value="350"></label>
<label>y2<input type="number" id="y2_" value="25"></label>
<label>x3<input type="number" id="x3_" value="350"></label>
<label>y3<input type="number" id="y3_" value="225"></label>
<label>x4<input type="number" id="x4_" value="10"></label>
<label>y4<input type="number" id="y4_" value="225"></label>
<label>lineWidth<input type="number" id="lineWidth_" value="3"></label>
</div>
<canvas id="c" width="400" height="400"></canvas>
So now, if you only want to use XXXRect, you can also create a single huge dash-array containing all of the dashes...
var ctx = c.getContext('2d');
ctx.lineCap = 'square';
function getRectDashes(width, height) {
var w_array = getLineDashes(width, 0, 0, 0);
var h_array = getLineDashes(0, height, 0, 0);
dashArray = [].concat.apply([], [w_array, 0, h_array, 0, w_array, 0, h_array]);
return dashArray;
}
// same as previous snippet except that it does return all the segment's dashes
function getLineDashes(x1, y1, x2, y2) {
var length = Math.hypot((x2 - x1), (y2 - y1));
var dash_length = length / 8;
var nb_of_dashes = length / dash_length;
var dash_gap = dash_length * 0.66666;
dash_length -= dash_gap * 0.3333;
var total_length = 0;
var dasharray = [];
var next;
while (total_length < length) {
next = dasharray.length % 2 ? dash_gap : dash_length;
total_length += next;
dasharray.push(next);
}
return dasharray;
}
function draw() {
ctx.clearRect(0, 0, c.width, c.height);
ctx.lineWidth = lineWidth_.value;
var w = width_.value,
h = height_.value;
ctx.setLineDash(getRectDashes(w, h));
ctx.strokeRect(20, 20, w, h);
}
draw();
document.oninput = function(e) {
if (e.target.parentNode.parentNode === inputs_)
draw();
};
label {
display: inline-block;
}
input {
max-width: 50px;
}
<div id="inputs_">
<label>width<input type="number" id="width_" value="200"></label>
<label>height<input type="number" id="height_" value="225"></label>
<label>lineWidth<input type="number" id="lineWidth_" value="3"></label>
</div>
<canvas id="c" width="400" height="400"></canvas>

Erasing previously drawn lines on an HTML5 canvas

To play around with HTML5 canvas, I decided to make an app which draws an analogue clockface. Everything's fine, except that old lines don't get erased in the way that I would expect. I've included part of the code below - DrawHands() gets called once a second:
var hoursPoint = new Object();
var minutesPoint = new Object();
var secondsPoint = new Object();
function drawHands()
{
var now = new Date();
drawLine(centerX, centerY, secondsPoint.X, secondsPoint.Y, "white", 1);
var seconds = now.getSeconds();
secondsPoint = getOtherEndOfLine(centerX, centerY, 2 * Math.PI / 60 * seconds, 0.75 * radius);
drawLine(centerX, centerY, secondsPoint.X, secondsPoint.Y, "black", 1);
drawLine(centerX, centerY, minutesPoint.X, minutesPoint.Y, "white", 3);
var minutes = now.getMinutes();
minutesPoint = getOtherEndOfLine(centerX, centerY, 2 * Math.PI / 60 * minutes, 0.75 * radius);
drawLine(centerX, centerY, minutesPoint.X, minutesPoint.Y, "black", 3);
drawLine(centerX, centerY, hoursPoint.X, hoursPoint.Y, "white", 3);
var hours = now.getHours();
if (hours >= 12) { hours -= 12; } // Hours are 0-11
hoursPoint = getOtherEndOfLine(centerX, centerY, (2 * Math.PI / 12 * hours) + (2 * Math.PI / 12 / 60 * minutes), 0.6 * radius);
drawLine(centerX, centerY, hoursPoint.X, hoursPoint.Y, "black", 3);
}
To make sense of the above, there are two helper functions:
drawLine(x1, y1, x2, y2, color, thickness)
getOtherEndOfLine(x, y, angle, length)
The problem is that while all the hands get drawn as expected in black, they never get erased. I would expect that since the same line is drawn in white (the background colour) it would effectively erase what was previously drawn at that point. But this doesn't seem to be the case.
Anything I'm missing?
Instead of erasing the things you don't want you can:
save the state of the canvas
draw the things you don't want
restore the canvas to the saved state to 'erase' them
This can be accomplished pretty easily using ImageData:
var canvas = document.querySelector('canvas'),
context = canvas.getContext('2d');
context.fillStyle = 'blue';
context.fillRect(0,0,200,200);
// save the state of the canvas here
var imageData = context.getImageData(0,0,canvas.width,canvas.height);
// draw a red rectangle that we'll get rid of in a second
context.fillStyle = 'red';
context.fillRect(50,50,100,100);
setTimeout(function () {
// return the canvas to the state right after we drew the blue rect
context.putImageData(imageData, 0, 0);
}, 1000);
<canvas width=200 height=200>
For reasons that I could expand upon, you should consider clearing your canvas and redrawing it entirely unless there are performance or compositing reasons not to.
You want clearRect, something like this:
//clear the canvas so we can draw a fresh clock
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
//redraw your clock here
/* ... */
The reason you can't just redraw the line in white and hope for it to erase the old line is because there might be some anti-aliasing/bleeding. You'll also notice that a straight horizontal line drawn on a pixel versus a half-pixel looks very different because of this.
When you do your white "erase" lines, try drawing them with a larger lineWidth by about 3 or 4. That should work for your case.
You should also draw all of the white lines first, then all of the black lines, in case they intersect.
A quick and easy way to clear a canvas is to set the width:
context.canvas.width = context.canvas.width;
My solution is double buffering :
var shapes =
[{type:"circle", x:50, y:50, radious:40, lineWidth:2, strokeStyle:"#FF0000", fillStyle:"#800000"}
,{type:"rectangle", x:50, y:50, width:100, height: 100, lineWidth:2, strokeStyle:"#00FF00", fillStyle:"#008000"}
,{type:"line", x1:75, y1:100, x2:170, y2:75, lineWidth:3, strokeStyle:"#0000FF"}
];
step1();
setTimeout(function () {
step2();
setTimeout(function () {
step3();
}, 1000);
}, 1000);
function step1() {
clearCanvas('myCanvas1');
shapes.forEach((sh) => { drawShape('myCanvas1', sh); });
};
function step2() {
clearCanvas('myCanvas2');
shapes.pop();
shapes.forEach((sh) => { drawShape('myCanvas2', sh); });
showOtherCanvas('myCanvas2', 'myCanvas1');
};
function step3() {
clearCanvas('myCanvas1');
shapes.pop();
shapes.forEach((sh) => { drawShape('myCanvas1', sh); });
showOtherCanvas('myCanvas1', 'myCanvas2');
};
function showOtherCanvas(cnv1, cnv2) {
var c1 = document.getElementById(cnv1);
var c2 = document.getElementById(cnv2);
c1.style['z-index'] = 3;
c2.style['z-index'] = 1;
c1.style['z-index'] = 2;
}
function clearCanvas(canvasID) {
var canvas = document.getElementById(canvasID);
var ctx = canvas.getContext('2d');
ctx.fillStyle="#FFFFFF";
ctx.fillRect(0,0,480,320);
}
function drawShape (canvasID, info) {
switch (info.type) {
case "line" : drawLine(canvasID, info);
case "rectangle" : drawRectangle(canvasID, info);
case "circle" : drawCircle(canvasID, info);
}
}
function drawLine (canvasID, info) {
var canvas = document.getElementById(canvasID);
var ctx = canvas.getContext('2d');
ctx.strokeStyle = info.strokeStyle;
ctx.lineWidth = info.lineWidth
ctx.beginPath();
ctx.moveTo(info.x1, info.y1);
ctx.lineTo(info.x2, info.y2);
ctx.stroke();
}
function drawRectangle (canvasID, info) {
var canvas = document.getElementById(canvasID);
var ctx = canvas.getContext('2d');
ctx.fillStyle = info.fillStyle;
ctx.strokeStyle = info.strokeStyle;
ctx.lineWidth = info.lineWidth
ctx.fillRect(info.x, info.y, info.width, info.height);
ctx.strokeRect(info.x, info.y, info.width, info.height);
}
function drawCircle (canvasID, info) {
var canvas = document.getElementById(canvasID);
var ctx = canvas.getContext('2d');
ctx.fillStyle = info.fillStyle;
ctx.strokeStyle = info.strokeStyle;
ctx.lineWidth = info.lineWidth
ctx.beginPath();
ctx.arc(info.x, info.y, info.radious, 0, 2 * Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(info.x, info.y, info.radious, 0, 2 * Math.PI);
ctx.stroke();
}
<canvas id="myCanvas2" width="480" height="320"
style="border: 1px solid #000000; position: absolute; top: 10; left: 10; z-index:1">
</canvas>
<canvas id="myCanvas1" width="480" height="320"
style="border: 1px solid #000000; position: absolute; top: 10; left: 10; z-index:2">
</canvas>
The change is so fast you won't see any flicker.

Categories