Canvas shape click removes more than one shape - javascript

I'm attempting to detect a canvas click and if the click coordinates match a shape. It works fine, the problem is that when the shapes overlap on the canvas (one shape might be smaller than the other) then both shapes are removed. Is there a way to avoid this and only remove one at a time?
addShape() {
const randomNum = (min, max) => Math.round(Math.random() * (max - min) + min),
randomRad = randomNum(10, 100),
randomX = randomNum(randomRad, this.canvas.width - randomRad);
let shape = new Shape(randomX, randomNum, randomRad);
shape.drawShape();
this.shapes.push(shape);
}
canvasClick() {
if(!this.paused) {
const canvasRect = event.target.getBoundingClientRect(),
clickedX = event.clientX - canvasRect.left,
clickedY = event.clientY - canvasRect.top;
for (let i = 0; i < this.shapes.length; i++) {
if(Math.pow(clickedX - this.shapes[i].x, 2) + Math.pow(clickedY - this.shapes[i].y, 2)
< Math.pow(this.shapes[i].rad,2)) {
this.shapes.splice(i, 1);
}
}
}
}
Thanks in advance for the help!

If I have understood what you what, the solution is pretty simple. Just break the loop after you remove one shape. Something like this:
for (let i = 0; i < this.shapes.length; i++) {
if (
Math.pow(clickedX - this.shapes[i].x, 2)
+ Math.pow(clickedY - this.shapes[i].y, 2)
< Math.pow(this.shapes[i].rad, 2)
) {
this.shapes.splice(i, 1);
break; // <--
}
}
You might also what to store the z position of shapes so that you can first sort them in the terms of z and then run this loop so that you don't remove shape below the clicked shape.

This is how I would do it. Please read the comments in the code. I am using the ctx.isPointInPath method but you may use the formula if you prefer.
The main idea is to exit the loop after finding and deleting the first circle.
I hope it helps.
let ctx = canvas.getContext("2d");
canvas.width = 300;
canvas.height = 300;
ctx.fillStyle = "rgba(0,0,0,.5)";
// the array of the circles
let circles = []
class Circle{
constructor(){
this.x = ~~(Math.random() * canvas.width);
this.y = ~~(Math.random() * canvas.height);
this.r = ~~(Math.random() * (40 - 10 + 1) + 10);
this.draw();
}
draw(){
ctx.beginPath();
ctx.arc(this.x,this.y, this.r, 0,2*Math.PI)
}
}
// create 20 circles
for(i=0;i<20;i++){
let c = new Circle();
ctx.fill()
circles.push(c)
}
canvas.addEventListener("click",(e)=>{
// detect the position of the mouse
let m = oMousePos(canvas, e)
for(let i=0;i<circles.length;i++){
// draw a circle but do not fill
circles[i].draw();
// check if the point is in path
if(ctx.isPointInPath(m.x,m.y)){
//remove the circle from the array
circles.splice(i, 1);
// clear the context
ctx.clearRect(0,0,canvas.width,canvas.height)
// redraw all the circles from the array
for(let j=0;j<circles.length;j++){
circles[j].draw();
ctx.fill()
}
//exit the loop
return;
}
}
})
// a function to detect the mouse position
function oMousePos(canvas, evt) {
var ClientRect = canvas.getBoundingClientRect();
return { //objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
canvas{border:1px solid}
<canvas id="canvas"></canvas>

Related

JavaScript Smoother Multiplication Circle

The Idea
I came across this idea of multiplication circles from a YouTube video that I stumbled upon and I thought that would be a fun thing to try and recreate using JavasSript and the canvas element.
The Original Video
The Problem
I smoothed out the animation the best I could but it still doesn't look as proper as I'd like. I suspect coming up with a solution would require a decent amount of math. To grasp the problem in detail I think it's easier to look at the code
window.onload = () => {
const app = document.querySelector('#app')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const { cos, sin, PI } = Math
const Tao = PI * 2
const width = window.innerWidth
const height = window.innerHeight
const cx = width / 2
const cy = height / 2
const baseNumberingSystem = 200
const stop = 34
let color = 'teal'
let multiplier = 0
let drawQue = []
// setup canvas
canvas.width = width
canvas.height = height
class Circle {
constructor(x, y, r, strokeColor, fillColor) {
this.x = x
this.y = y
this.r = r
this.strokeColor = strokeColor || '#fff'
this.fillColor = fillColor || '#fff'
}
draw(stroke, fill) {
ctx.moveTo(this.x, this.y)
ctx.beginPath()
ctx.arc(this.x, this.y, this.r, 0, Tao)
ctx.closePath()
if (fill) {
ctx.fillStyle = this.fillColor
ctx.fill()
}
if (stroke) {
ctx.strokeStyle = this.strokeColor
ctx.stroke()
}
}
createChildCircleAt(degree, radius, strokeColor, fillColor) {
const radian = degreeToRadian(degree)
const x = this.x + (this.r * cos(radian))
const y = this.y + (this.r * sin(radian))
return new Circle(x, y, radius, strokeColor, fillColor)
}
divideCircle(nTimes, radius) {
const degree = 360 / nTimes
let division = 1;
while (division <= nTimes) {
drawQue.push(this.createChildCircleAt(division * degree, radius))
division++
}
}
}
function degreeToRadian(degree) {
return degree * (PI / 180)
}
function draw() {
const mainCircle = new Circle(cx, cy, cy * 0.9)
// empty the que
drawQue = []
// clear canvas
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = "black"
ctx.fillRect(0, 0, width, height)
// redraw everything
mainCircle.draw()
mainCircle.divideCircle(baseNumberingSystem, 4)
drawQue.forEach(item => item.draw())
// draw modular times table
for (let i = 1; i <= drawQue.length; i++) {
const product = i * multiplier;
const firstPoint = drawQue[i]
const secondPoint = drawQue[product % drawQue.length]
if (firstPoint && secondPoint) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(secondPoint.x, secondPoint.y)
ctx.closePath()
ctx.stroke()
}
}
}
function animate() {
multiplier+= 0.1
multiplier = parseFloat(multiplier.toFixed(2))
draw()
console.log(multiplier, stop)
if (multiplier === stop) {
clearInterval(animation)
}
}
app.appendChild(canvas)
let animation = setInterval(animate, 120)
}
So the main issue comes from when we increment the multiplier by values less than 1 in an attempt to make the animation more fluid feeling. Example: multiplier+= 0.1. When we do this it increase the amount of times our if block in our draw function will fail because the secondPoint will return null.
const product = i * multiplier; // this is sometimes a decimal
const firstPoint = drawQue[i]
const secondPoint = drawQue[product % drawQue.length] // therefore this will often not be found
// Then this if block wont execute. Which is good because if it did we the code would crash
// But I think what we need is an if clause to still draw a line to a value in between the two
// closest indices of our drawQue
if (firstPoint && secondPoint) {
//...
}
Possible Solution
I think what I'd need to do is when we fail to find the secondPoint get the remainder of product % drawQue.length and use that to create a new circle in between the two closest circles in the drawQue array and use that new circle as the second point of our line.
If you use requestAnimationFrame it looks smooth
function animate() {
if (multiplier != stop) {
multiplier+= 0.1
multiplier = parseFloat(multiplier.toFixed(2))
draw()
requestAnimationFrame(animate);
}
}
app.appendChild(canvas)
animate()
My possible solution ended up working. I'll leave the added else if block here for anyone whos interested. I also had to store the degree value in my circle objects when they were made as well as calculate the distance between each subdivision of the circle.
Added If Else Statement
for (let i = 1; i <= drawQue.length; i++) {
const product = i * multiplier;
const newIndex = product % drawQue.length
const firstPoint = drawQue[i]
const secondPoint = drawQue[newIndex]
if (firstPoint && secondPoint) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(secondPoint.x, secondPoint.y)
ctx.closePath()
ctx.stroke()
} else if (!secondPoint) {
const percent = newIndex % 1
const closest = drawQue[Math.floor(newIndex)]
const newDegree = closest.degree + (degreeIncriments * percent)
const target = mainCircle.createChildCircleAt(newDegree, 4)
if (firstPoint && target) {
ctx.beginPath()
ctx.moveTo(firstPoint.x, firstPoint.y)
ctx.strokeStyle = color
ctx.lineTo(target.x, target.y)
ctx.closePath()
ctx.stroke()
}
}
Other changes
// ...
const degreeIncriments = 360 / baseNumberingSystem
// ...
class Circle {
constructor(/* ... */, degree )
// ...
this.degree = degree || 0
}
Hope someone finds this useful...

Need help to draw multiples balls inside a rect on canvas

I need to draw multiples balls inside a rect. I have a rect and 4 informations. Width and height of the rect.. numbers of balls per line and numbers of lines. That's been said I have to draw, for example, 4 balls at the same line. starting by the corners(That I was able to do) but I can't figure out how to draw more than 2 balls, example: If I have 3 balls, I need to draw 2 in the corners and 1 in the middle, if I have 4 balls... 2 in the corners and 2 in the middle. I had the idea of think about the rect as a matrix but having no luck.. link to see what I mean
If you need to drawn for example n dragon balls on line then you can divide length with n + 1 to get spacing between center of balls, or if you want different offset on start and end then you would divide (width - 2*offset) / (n - 1).
<canvas id="canvas" width="300" height="100">
</canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
class Rect {
constructor(x, y, width, heght) {
this.x = x;
this.y = y;
this.width = width;
this.heght = heght;
}
}
class Circle {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
}
class Scene
{
constructor() {
this.items = [];
}
clear() {
this.items = [];
}
add(item) {
this.items.push(item);
}
draw(ctx) {
for(let item of this.items) {
if (item instanceof Rect) {
ctx.beginPath();
ctx.rect(item.x, item.y, item.width, item.heght);
ctx.stroke();
} else if (item instanceof Circle) {
ctx.beginPath();
ctx.arc(item.x, item.y, item.radius, 0, 2 * Math.PI);
ctx.stroke();
}
}
}
}
const scene = new Scene();
scene.clear();
scene.add(new Rect(0, 0, 300, 100));
let n = 5;
let offset = 30;
let spacing = ((300 - 2 * offset ) / (n - 1));
for (let i = 0; i < n; i++) {
scene.add(new Circle(i * spacing + offset, 50, 25))
}
scene.draw(ctx);
</script>
I loved it and i'm using it now... although i'm having trouble trying to positioning the balls inside my draw... see what I got so far and if you have a little more time to give me a hand in this <3 (I need to put the balls inside the third rect only no matter what width or height the user enter)
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
scene.clear();
context.beginPath();
context.strokeRect(zoomedX(0), zoomedY(0), zoomed(width), zoomed(height));
context.strokeRect(zoomedX(55), zoomedY(55), zoomed(width-10), zoomed(height-10));
context.strokeRect(zoomedX(60), zoomedY(60), zoomed(width-20), zoomed(height-20));
context.closePath();
let radius = 8;
let n = 3;
let lines = 3;
let offset = 68;
let offsetY = 68;
let spacing = ((width - 2 * offset ) / (n - 1));
let spacingY = ((height - 2 * offset ) / (lines - 1));
for (let i = 0; i < n; i++) {
for(let j = 0; j < lines ;j++){
scene.add(new Circle(i * spacing + offset, j * spacingY + offset, radius))
}
}
scene.draw(context);
}

Canvas Transparency creating perminant after-image

I am trying to achieve a tracing effect where the lines have a faded trail. The way I am trying to do it is simply by drawing the solid background once, and then on further frames draw a transparent background before drawing the new lines, so that you can still see a little of the image before it.
The issue is that I do want the lines to fade out completely after some time, but they seem to leave a permanent after image, even after drawing over them repeatedly.
I've tried setting different globalCompositeOperation(s) and it seemed like I was barking up the wrong tree there.
This code is called once
//initiate trace bg
traceBuffer.getContext("2d").fillStyle = "rgba(0, 30, 50, 1)";
traceBuffer.getContext("2d").fillRect(0, 0, traceBuffer.width, traceBuffer.height);
then inside the setInterval function it calls
//draw transparent background
ctx.fillStyle = "rgba(0, 30, 50, 0.04)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//set drawing settings
ctx.strokeStyle = "#AAAAAA";
ctx.lineWidth = 4;
for (let i = 0; i < tracer.layer2.length; i++){
ctx.beginPath();
ctx.moveTo(newX, newY);
ctx.lineTo(oldX, oldY);
ctx.stroke();
}
Here's an example: https://i.imgur.com/QTkeIVf.png
On the left is what I am currently getting, and on the right is the edit of what I actually want to happen.
This is how I would do it. I would build a history of the particles moving on the track. The older the position the smaller the value of the alpha value for the fill. Also for a nicer effect I would reduce the size of the circle.
I hope this is what you need.
PS: I would have loved to have your curve. Since I don't have it I've drawn a different one.
const hypotrochoid = document.getElementById("hypotrochoid");
const ctx = hypotrochoid.getContext("2d");
const cw = (hypotrochoid.width = 300);
const ch = (hypotrochoid.height = 300);
const cx = cw / 2,
cy = ch / 2;
ctx.lineWidth = 1;
ctx.strokeStyle = "#d9d9d9";
// variables for the hypotrochoid
let a = 90;
let b = 15;
let h = 50;
// an array where to save the points used to draw the track
let track = [];
//add points to the track array. This will be used to draw the track for the particles
for (var t = 0; t < 2 * Math.PI; t += 0.01) {
let o = {};
o.x = cx + (a - b) * Math.cos(t) + h * Math.cos((a - b) / b * t);
o.y = cy + (a - b) * Math.sin(t) - h * Math.sin((a - b) / b * t);
track.push(o);
}
// a function to draw the track
function drawTrack(ry) {
ctx.beginPath();
ctx.moveTo(ry[0].x, ry[0].y);
for (let t = 1; t < ry.length; t++) {
ctx.lineTo(ry[t].x, ry[t].y);
}
ctx.closePath();
ctx.stroke();
}
// a class of points that are moving on the track
class Point {
constructor(pos) {
this.pos = pos;
this.r = 3;//the radius of the circle
this.history = [];
this.historyLength = 40;
}
update(newPos) {
let old_pos = {};
old_pos.x = this.pos.x;
old_pos.y = this.pos.y;
//save the old position in the history array
this.history.push(old_pos);
//if the length of the track is longer than the max length allowed remove the extra elements
if (this.history.length > this.historyLength) {
this.history.shift();
}
//gry the new position on the track
this.pos = newPos;
}
draw() {
for (let i = 0; i < this.history.length; i++) {
//calculate the alpha value for every element on the history array
let alp = i * 1 / this.history.length;
// set the fill style
ctx.fillStyle = `rgba(0,0,0,${alp})`;
//draw an arc
ctx.beginPath();
ctx.arc(
this.history[i].x,
this.history[i].y,
this.r * alp,
0,
2 * Math.PI
);
ctx.fill();
}
}
}
// 2 points on the track
let p = new Point(track[0]);
let p1 = new Point(track[~~(track.length / 2)]);
let frames = 0;
let n, n1;
function Draw() {
requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
//indexes for the track position
n = frames % track.length;
n1 = (~~(track.length / 2) + frames) % track.length;
//draw the track
drawTrack(track);
// update and draw the first point
p.update(track[n]);
p.draw();
// update and draw the second point
p1.update(track[n1]);
p1.draw();
//increase the frames counter
frames++;
}
Draw();
canvas{border:1px solid}
<canvas id="hypotrochoid"></canvas>

How to clear the canvas without interrupting animations?

I am visualising flight paths with D3 and Canvas. In short, I have data for each flight's origin and destination
as well as the airport coordinates. The ideal end state is to have an indiviudal circle representing a plane moving
along each flight path from origin to destination. The current state is that each circle gets visualised along the path,
yet the removal of the previous circle along the line does not work as clearRect gets called nearly constantly.
Current state:
Ideal state (achieved with SVG):
The Concept
Conceptually, an SVG path for each flight is produced in memory using D3's custom interpolation with path.getTotalLength() and path.getPointAtLength() to move the circle along the path.
The interpolator returns the points along the path at any given time of the transition. A simple drawing function takes these points and draws the circle.
Key functions
The visualisation gets kicked off with:
od_pairs.forEach(function(el, i) {
fly(el[0], el[1]); // for example: fly('LHR', 'JFK')
});
The fly() function creates the SVG path in memory and a D3 selection of a circle (the 'plane') - also in memory.
function fly(origin, destination) {
var pathElement = document.createElementNS(d3.namespaces.svg, 'path');
var routeInMemory = d3.select(pathElement)
.datum({
type: 'LineString',
coordinates: [airportMap[origin], airportMap[destination]]
})
.attr('d', path);
var plane = custom.append('plane');
transition(plane, routeInMemory.node());
}
The plane gets transitioned along the path by the custom interpolater in the delta() function:
function transition(plane, route) {
var l = route.getTotalLength();
plane.transition()
.duration(l * 50)
.attrTween('pointCoordinates', delta(plane, route))
// .on('end', function() { transition(plane, route); });
}
function delta(plane, path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
draw([p.x, p.y]);
};
};
}
... which calls the simple draw() function
function draw(coords) {
// contextPlane.clearRect(0, 0, width, height); << how to tame this?
contextPlane.beginPath();
contextPlane.arc(coords[0], coords[1], 1, 0, 2*Math.PI);
contextPlane.fillStyle = 'tomato';
contextPlane.fill();
}
This results in an extending 'path' of circles as the circles get drawn yet not removed as shown in the first gif above.
Full code here: http://blockbuilder.org/larsvers/8e25c39921ca746df0c8995cce20d1a6
My question is, how can I achieve to draw only a single, current circle while the previous circle gets removed without interrupting other circles being drawn on the same canvas?
Some failed attempts:
The natural answer is of course context.clearRect(), however, as there's a time delay (roughly a milisecond+) for each circle to be drawn as it needs to get through the function pipeline clearRect gets fired almost constantly.
I tried to tame the perpetual clearing of the canvas by calling clearRect only at certain intervals (Date.now() % 10 === 0 or the like) but that leads to no good either.
Another thought was to calculate the previous circle's position and remove the area specifically with a small and specific clearRect definition within each draw() function.
Any pointers very much appreciated.
Handling small dirty regions, especially if there is overlap between objects quickly becomes very computationally heavy.
As a general rule, a average Laptop/desktop can easily handle 800 animated objects if the computation to calculate position is simple.
This means that the simple way to animate is to clear the canvas and redraw every frame. Saves a lot of complex code that offers no advantage over the simple clear and redraw.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x += p.ax * p.speed;
p.y += p.ay * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(800,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
As pointed out in the comments some machines may be able to draw a circle if one colour and all as one path slightly quicker (not all machines). The point of rendering an image is that it is invariant to the image complexity. Image rendering is dependent on the image size but colour and alpha setting per pixel have no effect on rendering speed. Thus I have changed the circle to show the direction of each point via a little plane icon.
Path follow example
I have added a way point object to each plane that in the demo has a random set of way points added. I called it path (could have used a better name) and a unique path is created for each plane.
The demo is to just show how you can incorporate the D3.js interpolation into the plane update function. The plane.update now calls the path.getPos(time) which returns true if the plane has arrived. If so the plane is remove. Else the new plane coordinates are used (stored in the path object for that plane) to set the position and direction.
Warning the code for path does little to no vetting and thus can easily be made to throw an error. It is assumed that you write the path interface to the D3.js functionality you want.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const path = {
wayPoints : null, // holds way points
nextTarget : null, // holds next target waypoint
current : null, // hold previously passed way point
x : 0, // current pos x
y : 0, // current pos y
addWayPoint(x,y,time){
this.wayPoints.push({x,y,time});
},
start(){
if(this.wayPoints.length > 1){
this.current = this.wayPoints.shift();
this.nextTarget = this.wayPoints.shift();
}
},
getNextTarget(){
this.current = this.nextTarget;
if(this.wayPoints.length === 0){ // no more way points
return;
}
this.nextTarget = this.wayPoints.shift(); // get the next target
},
getPos(time){
while(this.nextTarget.time < time && this.wayPoints.length > 0){
this.getNextTarget(); // get targets untill the next target is ahead in time
}
if(this.nextTarget.time < time){
return true; // has arrivecd at target
}
// get time normalised ove time between current and next
var timeN = (time - this.current.time) / (this.nextTarget.time - this.current.time);
this.x = timeN * (this.nextTarget.x - this.current.x) + this.current.x;
this.y = timeN * (this.nextTarget.y - this.current.y) + this.current.y;
return false; // has not arrived
}
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
var p;
planes.items.push(p = {
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
path : Object.assign({},path,{wayPoints : []}),
})
return p; // return the plane
},
update(time){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
if(p.path.getPos(time)){ // target reached
planes.items.splice(i--,1); // remove
}else{
p.dir = Math.atan2(p.y - p.path.y, p.x - p.path.x) + Math.PI; // add 180 because i drew plane wrong way around.
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x = p.path.x;
p.y = p.path.y;
}
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(time){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(810,()=>{
var p = planes.add(Math.random() * canvas.width, Math.random() * canvas.height);
// now add random number of way points
var timeP = time;
// info to create a random path
var dir = Math.random() * Math.PI * 2;
var x = p.x;
var y = p.y;
doFor(Math.floor(Math.random() * 80 + 12),()=>{
var dist = Math.random() * 5 + 4;
x += Math.cos(dir) * dist;
y += Math.sin(dir) * dist;
dir += (Math.random()-0.5)*0.3;
timeP += Math.random() * 1000 + 500;
p.path.addWayPoint(x,y,timeP);
});
// last waypoin at center of canvas.
p.path.addWayPoint(canvas.width / 2,canvas.height / 2,timeP + 5000);
p.path.start();
})
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update(time);
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
#Blindman67 is correct, clear and redraw everything, every frame.
I'm here just to say that when dealing with such primitive shapes as arc without too many color variations, it's actually better to use the arc method than drawImage().
The idea is to wrap all your shapes in a single path declaration, using
ctx.beginPath(); // start path declaration
for(i; i<shapes.length; i++){ // loop through our points
ctx.moveTo(pt.x + pt.radius, pt.y); // default is lineTo and we don't want it
// Note the '+ radius', arc starts at 3 o'clock
ctx.arc(pt.x, pt.y, pt.radius, 0, Math.PI*2);
}
ctx.fill(); // a single fill()
This is faster than drawImage, but the main caveat is that it works only for single-colored set of shapes.
I've made an complex plotting app, where I do draw a lot (20K+) of entities, with animated positions. So what I do, is to store two sets of points, one un-sorted (actually sorted by radius), and one
sorted by color. I then do use the sorted-by-color one in my animations loop, and when the animation is complete, I draw only the final frame with the sorted-by-radius (after I filtered the non visible entities). I achieve 60fps on most devices. When I tried with drawImage, I was stuck at about 10fps for 5K points.
Here is a modified version of Blindman67's good answer's snippet, using this single-path approach.
/* All credits to SO user Blindman67 */
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
const planes = {
items : [],
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
rad: 2,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.x += Math.cos(p.dir) * p.speed;
p.y += Math.sin(p.dir) * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
ctx.beginPath();
ctx.fillStyle = 'red';
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.moveTo(x + p.rad, y)
ctx.arc(x, y, p.rad, 0, Math.PI*2);
}
ctx.fill();
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(8000,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
z-index: -1;
}
<canvas id=canvas></canvas>
8000 animated points
Not directly related but in case you've got part of your drawings that don't update at the same rate as the rest (e.g if you want to highlight an area of your map...) then you might also consider separating your drawings in different layers, on offscreen canvases. This way you'd have one canvas for the planes, that you'd clear every frame, and other canvas for other layers that you would update at different rate. But that's an other story.

How do I leave a trail behind my shapes in canvas?

Right now I have shapes that are created when the user spins the mouse wheel and they slowly fade away after a certain amount of time. How would I generate a trail behind each shape that follows it and also slowly disappears? Here's the code:
var canvas;
var context;
var triangles = [];
var timer;
function init() {
canvas = document.getElementById('canvas');
context = canvas.getContext("2d");
resizeCanvas();
window.addEventListener('resize', resizeCanvas, false);
window.addEventListener('orientationchange', resizeCanvas, false);
canvas.onwheel = function(event) {
handleClick(event.clientX, event.clientY);
};
var timer = setInterval(resizeCanvas, 30);
}
function Triangle(x, y, triangleColor) {
this.x = x;
this.y = y;
this.triangleColor = triangleColor;
this.vx = Math.random() * 30 - 15;
this.vy = Math.random() * 30 - 15;
this.time = 100;
}
function handleClick(x, y) {
var colors = [
[0, 170, 255],
[230, 180, 125],
[50, 205, 130]
];
var triangleColor = colors[Math.floor(Math.random() * colors.length)];
triangles.push(new Triangle(x, y, triangleColor));
for (var i = 0; i < triangles.length; i++) {
drawTriangle(triangles[i]);
}
}
function drawTriangle(triangle) {
context.beginPath();
context.moveTo(triangle.x, triangle.y);
context.lineTo(triangle.x + 25, triangle.y + 25);
context.lineTo(triangle.x + 25, triangle.y - 25);
var c = triangle.triangleColor
context.fillStyle = 'rgba(' + c[0] + ', ' + c[1] + ', ' + c[2] + ', ' + (triangle.time / 100) + ')';
context.fill();
}
function resizeCanvas() {
canvas.width = window.innerWidth - 20;
canvas.height = window.innerHeight - 20;
fillBackgroundColor();
for (var i = 0; i < triangles.length; i++) {
var t = triangles[i];
drawTriangle(t);
if (t.x + t.vx > canvas.width || t.x + t.vx < 0)
t.vx = -t.vx
if (t.y + t.vy > canvas.height || t.y + t.vy < 0)
t.vy = -t.vy
if (t.time === 0) {
triangles.splice(i, 1);
}
t.time -= 1;
t.x += t.vx;
t.y += t.vy;
}
}
function fillBackgroundColor() {
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);
}
init()
<canvas id="canvas" width="500" height="500"></canvas>
Well, it's Sunday. Here's a way of doing it that works:
Add an empty array and opacity property to triangle object properties in the constructor, calling them, say, trails and alpha respectively.
Define the maximum number of ghost triangles you want in the trail. I found 3 a good value:
var MAX_GHOSTS = 3;
Make the triangle drawing function take an alpha channel value as its second parameter and remove opacity calculation from within the function itself:
function drawTriangle(triangle, alpha) { // two parameters required }
Where triangles are currently drawn in the timer callback (within the loop), calculate and update the triangle's alpha value used in the call to drawTriangle. Push a copy of selected triangle properties onto the triangle's trails array: copying freezes the x,y position. Not pushing every timer call spaces the ghost images further apart:
if( t.time%2 == 0) {
t.trails.push(
{ x: t.x, y: t.y, alpha: t.alpha, triangleColor: t.triangleColor}
);
}
Also in the timer call back, before the existing drawing loop, add a new double nested loop structure where
the outer loop retrieves the trails array of each entry in the triangles array. If the trails array length is more than MAX_GHOST, remove its first element (using splice).
the inner loop, using say j as the index, gets the next elements from the trails array, calling it crumb for example. Calculate an alpha value to make it fade quickly and draw it:
drawTriangle( crumb, (j+1)/(MAX_GHOSTS + 2) * crumb.alpha);
Hope you enjoy!

Categories