Fade out tiles in canvas image - javascript

Still learning Canvas. How can I fade out each tile in an image that I have as Image Data in an array:
for (let i = 0; i < tilesY; i++) {
for (let j = 0; j < tilesX; j++) {
this.ctxTile.putImageData(this.tileData[itr], j * tileWidth, i * tileHeight);
itr += 1
}
}
I understand that the solution must have something to do with compositing? I want to fade out each tile individually. The putImageData works and the image is inside canvas, and assembled as a set of tiles.
Thanks

Usually you'd just play with the ctx.globalAlpha property at the time of drawing to your context to set your tile's alpha. However, putImageData is kind of a strange beast in the API in that it ignores the context transformation, clipping areas and in our case compositing rules, including globalAlpha.
So one hack around would be to "erase" the given tile after we've drawn it. For this we can use the globalCompositeOperation = "destination-out" property that we'll use on a call to a simple fillRect() with the inverse globalAlpha we want. (Luckily putImageData always draws only rectangles).
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const tileWidth = 50;
const tileHeight = 50;
class Tile {
constructor(x, y, width, height) {
this.x = Math.round(x); // putImageData only renders at integer coords
this.y = Math.round(y);
this.width = width;
this.height = height;
this.img = buildImageData(width, height);
this.alpha = 1;
}
isPointInPath(x, y) {
return x >= this.x && x <= this.x + this.width &&
y >= this.y && y <= this.y + this.height;
}
draw() {
ctx.putImageData(this.img, this.x, this.y);
ctx.globalAlpha = 1 - this.alpha; // inverse alpha
// the next drawing will basically erase what it represents
ctx.globalCompositeOperation = "destination-out";
ctx.fillRect(this.x, this.y, this.width, this.height);
// restore the context
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
}
const tiles = Array.from({ length: 5 }, (_, i) => new Tile(i * tileWidth * 1.25, 0, tileWidth, tileHeight));
canvas.onclick = (e) => {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
const clickedTile = tiles.find((tile) => tile.isPointInPath(x, y));
if (clickedTile) { clickedTile.alpha -= 0.1 };
redraw();
};
redraw();
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
tiles.forEach((tile) => tile.draw());
}
function buildImageData(width, height) {
const img = new ImageData(width, height);
const arr = new Uint32Array(img.data.buffer);
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
}
return img;
}
<p>Click on each tile to lower its alpha.</p>
<canvas></canvas>
However this means that for each tile we have one putImageData + one composited fillRect. If you've got a lot of tiles, that makes for a pretty big overhead.
So instead the best might be to convert all your ImageData objects to ImageBitmap ones. To understand the difference between both I invite you to read this answer of mine.
Once we have ImageBitmaps, we can apply the globalAlpha on our draw call directly:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const tileWidth = 50;
const tileHeight = 50;
class Tile {
constructor(x, y, width, height) {
this.x = Math.round(x); // putImageData only renders at integer coords
this.y = Math.round(y);
this.width = width;
this.height = height;
this.alpha = 1;
const imgData = buildImageData(width, height);
// createImageBitmap is "async"
this.ready = createImageBitmap(imgData)
.then((bmp) => this.img = bmp);
}
isPointInPath(x, y) {
return x >= this.x && x <= this.x + this.width &&
y >= this.y && y <= this.y + this.height;
}
draw() {
// single draw per tile
ctx.globalAlpha = this.alpha;
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
ctx.globalAlpha = 1;
}
}
const tiles = Array.from({ length: 5 }, (_, i) => new Tile(i * tileWidth * 1.25, 0, tileWidth, tileHeight));
canvas.onclick = (e) => {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
const clickedTile = tiles.find((tile) => tile.isPointInPath(x, y));
if (clickedTile) { clickedTile.alpha -= 0.1 };
redraw();
};
// wait for all the ImageBitmaps are generated
Promise.all(tiles.map((tile) => tile.ready)).then(redraw);
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
tiles.forEach((tile) => tile.draw());
}
function buildImageData(width, height) {
const img = new ImageData(width, height);
const arr = new Uint32Array(img.data.buffer);
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
}
return img;
}
<p>Click on each tile to lower its alpha.</p>
<canvas></canvas>

Related

Can't animate multiple instances of an element with JavaScript. Only the first animation will be executed

I have created an element called Particle. Single animation of this element also works. But as soon as I try to run multiple animations, only one animation gets performed. I think the problem is the requestAnimationFrame (this.animate.bind(this))-call, but I don't know how to change it to accept multiple animations at once. Any ideas on how to fix this?
Code:
//gloabl vars
let particels = [];
let numberParticels = 120;
let canvas;
let ctx;
let title;
let mesaureTitle;
let boundRadius;
let animations;
window.onload = function () {
this.init();
for(let i = 0; i < numberParticels; i++){
particels[i].update();
particels[i].draw();
particels[i].animate(0);
}
}
function init(){
canvas = document.getElementById("c");
ctx = canvas.getContext("2d");
title = document.getElementById("title");
mesaureTitle = title.getBoundingClientRect();
bound = {
x: mesaureTitle.x,
y: mesaureTitle.y,
width: mesaureTitle.width,
height: mesaureTitle.height,
};
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
for(let i = 0; i < numberParticels; i++){
let x = Math.floor(Math.random() * window.innerWidth);
let y = Math.floor(Math.random() * window.innerHeight);
let size = Math.floor(Math.random() * 25) + 3;
let weight = Math.floor(Math.random() * 11) + 2;
particels.push(new Particel(x, y, size, weight));
}
}
class Particel {
constructor (x,y,size, weight) {
this.x = x;
this.y = y;
this.size = size;
this.directionX = 0.15332;
this.resetWeight = weight;
this.weight = weight;
this.lastTime = 0;
this.interval = 1000/60;
this.timer = 0;
}
update(){
this.weight += 0.02;
this.y = this.y + this.weight;
this.x += this.directionX;
//check for collision with textField
if (this.x < bound.x + bound.width
&& this.x + this.size > bound.x &&
this.y < bound.y + bound.height &&
this.y + this.size > bound.y) {
this.y -= 3;
this.weight *= -0.3;
}
}
draw(){
if(this.y > canvas.height){
this.y = 0 - this.size;
this.weight = this.resetWeight;
//create random start point
this.x = Math.floor(Math.random() * canvas.width);
}
ctx.fillStyle = "rgb(0, 180, 97)";
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
animate(timeStamp){
const deltaTime = timeStamp - this.lastTime;
this.lastTime = timeStamp;
if(this.timer > this.interval){
ctx.clearRect(0,0, canvas.width, canvas.height);
this.update();
this.draw();
this.timer = 0;
}else {
this.timer += deltaTime;
}
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
requestAnimationFrame(this.animate.bind(this));
}
}
The major problem is clearing the canvas inside the animate method of each particle. So if you draw multiple particles, each particle update call clears the canvas, overwriting previous particle data, which only leaves the last particle visible.
You could try removing the
ctx.clearRect(0,0, canvas.width, canvas.height);
line from where it is and create a createAnimationFrame call back in init to clear the canvas before amimating particles:
function init() {
// ....
requestAnimationFrame( ()=> ctx.clearRect(0,0, canvas.width, canvas.height));
// existing for loop:
for(let i = 0; i < numberParticels; i++){
// ... .
}
However this creates (one plus the number of particles) requests for an animation frame. A better solution would be to remove requesting animation frames from the Particel class and create a single requestAnimationFrame callback which goes through the particels array and calls a class method to redraw each particle on the canvas with updated position.
Also the code generates an error in strict mode that bound has not been declared. I suggest declaring it globally rather than relying on sloppy mode JavaScript creating it as a window property for you.

Improving Canvas draw call Performance

I want to draw quite a few dots. That's what I do:
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
function render () {
window.requestAnimationFrame(render);
// clear screen
ctx.clearRect(0, 0, cwidth, cheight);
for (let p of particles) {
p.rmove();
p.render(ctx);
}
}
render();
The drawing function for my point looks like this:
class Point {
x: number;
y: number;
constructor(x, y) {
this.x = x;
this.y = y;
}
rmove() {
this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
}
render (ctx) {
ctx.fillStyle = "gray";
ctx.beginPath();
ctx.rect(this.x, this.y, 1.5,1.5);
ctx.fill();
ctx.stroke();
}
}
Note that I round the values in the rmove() function, as canvas draws points with integer coordinates more quickly.
I'd like to somehow put all these drawing calls together.
Make your points trace on the given context (could even be a Path2D), and keep the actual drawing for the renderer.
All your points have to do is to make the context moveTo their own coords before tracing the rect.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
rmove() {
this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
}
trace (ctx) {
ctx.moveTo( this.x, this.y );
ctx.rect(this.x, this.y, 1.5, 1.5);
}
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cwidth = canvas.width = 300;
const cheight = canvas.height = 300;
const particles = Array.from(
{ length: 5000 },
()=> new Point( cwidth/2, cheight/2 )
);
function animate () {
update();
draw();
window.requestAnimationFrame(animate);
}
function update() {
for (let p of particles) {
p.rmove();
}
}
function draw() {
// clear screen
ctx.clearRect(0, 0, cwidth, cheight);
// define our single path
ctx.beginPath();
for (let p of particles) {
p.trace(ctx);
}
ctx.fillStyle = "gray";
ctx.stroke(); // OP has it reversed, but then the fill-color is almost not visible
// (1.5 width - 2*0.5 stroke leaves only 0.5 for the fill => antialiased...
ctx.fill();
}
window.requestAnimationFrame( animate );
<canvas id="canvas"></canvas>
But this works only because all your particles share the same color. If they didn't, then you'd need a bit more logic:
const colors = ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow'];
class Point {
constructor(x, y, color=0) {
this.x = x;
this.y = y;
this.color = color;
}
rmove() {
this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
}
trace (ctx) {
ctx.moveTo( this.x, this.y );
ctx.rect(this.x, this.y, 1.5, 1.5);
}
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cwidth = canvas.width = 300;
const cheight = canvas.height = 300;
const particles = Array.from(
{ length: 5000 },
()=> new Point( cwidth/2, cheight/2, (Math.random()*colors.length-1)|0 )
);
function animate () {
update();
draw();
window.requestAnimationFrame(animate);
}
function update() {
for (let p of particles) {
p.rmove();
}
}
function draw() {
// clear screen
ctx.clearRect(0, 0, cwidth, cheight);
// define our single path
let last_color = -1;
for (let p of particles) {
let p_color = p.color;
if( p_color !== last_color ) {
paint();
last_color = p_color;
}
p.trace(ctx);
}
paint(); // the last
function paint() {
ctx.fillStyle = colors[ last_color ];
ctx.strokeStyle = colors[ (last_color + 1) % colors .length ];
ctx.stroke();
ctx.fill();
ctx.beginPath();
}
}
window.requestAnimationFrame( animate );
<canvas id="canvas"></canvas>
Though doing this, you may very well end up with a lot of drawings, so a final trick which might not work everywhere is to sort your particles by their color. This results in a different graphic since this one color will always be at the top, but it might work in some cases and the performance gain can outfit the downside.
const colors = ['red', 'green', 'blue', 'cyan', 'magenta', 'yellow'];
class Point {
constructor(x, y, color=0) {
this.x = x;
this.y = y;
this.color = color;
}
rmove() {
this.x += Math.round(Math.random() < 0.5 ? -1 : 1);
this.y += Math.round(Math.random() < 0.5 ? -1 : 1);
}
trace (ctx) {
ctx.moveTo( this.x, this.y );
ctx.rect(this.x, this.y, 1.5, 1.5);
}
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const cwidth = canvas.width = 300;
const cheight = canvas.height = 300;
const particles = Array.from(
{ length: 5000 },
()=> new Point( cwidth/2, cheight/2, (Math.random()*colors.length-1)|0 )
);
particles.sort( (a, b) => a.color - b.color );
function animate () {
update();
draw();
window.requestAnimationFrame(animate);
}
function update() {
for (let p of particles) {
p.rmove();
}
}
function draw() {
// clear screen
ctx.clearRect(0, 0, cwidth, cheight);
// define our single path
let last_color = -1;
for (let p of particles) {
let p_color = p.color;
if( p_color !== last_color ) {
paint();
last_color = p_color;
}
p.trace(ctx);
}
paint(); // the last
function paint() {
ctx.fillStyle = colors[ last_color ];
ctx.strokeStyle = colors[ (last_color + 1) % colors .length ];
ctx.stroke();
ctx.fill();
ctx.beginPath();
}
}
window.requestAnimationFrame( animate );
<canvas id="canvas"></canvas>
And nothing prevents you to generate chunks from these sorted particles so it looks more random.

How to detect when mouse is outside of a certain circle?

When a mouse is hovering a image. It gets detect by this if statement:
if ((distance(circles[this.index].x, circles[this.index].y, mouse.x, mouse.y)) < circles[this.index].radius)
I also want to detect when a mouse it outside a image.
After that previous if statement I cannot use else the reason is because:
When I generate multiple images on screen and when my mouse if hovering over 1 image. It does hover of that image and the code detects it but it also doesnt hover of all the other images. That is the reason that is display 4 times "outside circle" and 1 time "inside circle"
As seen in the log:
Console.log output:
Mouse inside circle
Mouse outside circle 4
Mouse inside circle
Mouse outside circle 4
Im looking for a way the detect when the mouse is leaving a circle.
You can find the code I'm working with below:
PS: it it important that it detect in what (index) circle the mouse is and leaves.
I want to create a huge amount of pictures, but in the code below I used 5 for demo purpeses.
var mouse = {
x: innerWidth / 2,
y: innerHeight / 2
};
// Mouse Event Listeners
addEventListener('mousemove', event => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
//Calculate distance between 2 objects
function distance(x1, y1, x2, y2) {
let xDistance = x2 - x1;
let yDistance = y2 - y1;
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}
// Sqaure to circle
function makeCircleImage(radius, src, callback) {
var canvas = document.createElement('canvas');
canvas.width = canvas.height = radius * 2;
var ctx = canvas.getContext("2d");
var img = new Image();
img.src = src;
img.onload = function() {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// we use compositing, offers better antialiasing than clip()
ctx.globalCompositeOperation = 'destination-in';
ctx.arc(radius, radius, radius, 0, Math.PI*2);
ctx.fill();
callback(canvas);
};
}
function Circle( x, y, radius, index ) {
//Give var for circle
this.x = x;
this.y = y;
this.dx = 1;
this.dy = 1;
this.radius = radius;
this.index = index;
}
// use prototyping if you wish to make it a class
Circle.prototype = {
//Draw circle on canvas
draw: function () {
var
x = (this.x - this.radius),
y = (this.y - this.radius);
// draw is a single call
c.drawImage( this.image, x, y );
},
//Updates position of images
update: function () {
var
max_right = canvas.width + this.radius,
max_left = this.radius * -1;
this.x += this.dx;
if( this.x > max_right ) {
this.x += max_right - this.x;
this.dx *= -1;
}
if( this.x < max_left ) {
this.x += max_left - this.x;
this.dx *= -1;
}
if ((distance(circles[this.index].x, circles[this.index].y, mouse.x, mouse.y)) < circles[this.index].radius) {
// Mouse inside circle
console.log("Mouse inside circle")
} else{
//The mouse is in one circle
//And out of 4 other circles
console.log("Mouse outside circle")
}
},
init: function(callback) {
var url = "https://t4.ftcdn.net/jpg/02/26/96/25/240_F_226962583_DzHr45pyYPdmwnjDoqz6IG7Js9AT05J4.jpg";
makeCircleImage( this.radius, url, function(img) {
this.image = img;
callback();
}.bind(this));
}
};
//Animate canvas
function animate() {
c.clearRect(0, 0, window.innerWidth, window.innerHeight);
circles.forEach(function( circle ) {
circle.update();
});
circles.forEach(function( circle ) {
circle.draw();
});
requestAnimationFrame(animate);
}
//Init canvas
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//init circle objects
var circles = [
new Circle(10, 100, 50,0),
new Circle(10, 200, 30,1),
new Circle(10, 300, 50,2),
new Circle(10, 400, 50,3),
new Circle(10, 500, 50,4)
];
var ready = 0;
circles.forEach(function(circle) {
circle.init(oncircledone);
});
function oncircledone() {
if(++ready === circles.length) {
animate()
}
}
<canvas></canvas>
just add another property to circle
function Circle(x, y, radius, index) {
//Give var for circle
this.x = x;
this.y = y;
this.dx = 1;
this.dy = 1;
this.radius = radius;
this.index = index;
this.mouseInside = false
}
and then the update logic change to this
if ((distance(this.x, this.y, mouse.x, mouse.y)) < circles[this.index].radius) {
if (!this.mouseInside) {
this.mouseInside = true
console.log(`mouse enter circele at ${this.index}`)
}
}
else if (this.mouseInside) {
this.mouseInside = false
console.log(`mouse leave circele at ${this.index}`)
}
check if circles overlap and the you can decide if you want to update
var overlapsCircles = circles.filter(circle => {
var diffrentId = circle.index != this.index
var overlapping =
distance(this.x, this.y, circle.x, circle.y) < this.radius
return diffrentId && overlapping
})
if (overlapsCircles.length > 0) {
var overlapCircle = overlapsCircles.map(circle => circle.index)
console.log('overlap circle with index ' + overlapCircle)
}
var mouse = {
x: innerWidth / 2,
y: innerHeight / 2
};
// Mouse Event Listeners
addEventListener('mousemove', event => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
//Calculate distance between 2 objects
function distance(x1, y1, x2, y2) {
let xDistance = x2 - x1;
let yDistance = y2 - y1;
return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2));
}
// Sqaure to circle
function makeCircleImage(radius, src, callback) {
var canvas = document.createElement('canvas');
canvas.width = canvas.height = radius * 2;
var ctx = canvas.getContext("2d");
var img = new Image();
img.src = src;
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// we use compositing, offers better antialiasing than clip()
ctx.globalCompositeOperation = 'destination-in';
ctx.arc(radius, radius, radius, 0, Math.PI * 2);
ctx.fill();
callback(canvas);
};
}
function Circle(x, y, radius, index) {
//Give var for circle
this.x = x;
this.y = y;
this.dx = 1;
this.dy = 1;
this.radius = radius;
this.index = index;
this.mouseInside = false
}
// use prototyping if you wish to make it a class
Circle.prototype = {
//Draw circle on canvas
draw: function () {
var
x = (this.x - this.radius),
y = (this.y - this.radius);
// draw is a single call
c.drawImage(this.image, x, y);
},
//Updates position of images
update: function () {
var
max_right = canvas.width + this.radius,
max_left = this.radius * -1;
this.x += this.dx;
if (this.x > max_right) {
this.x += max_right - this.x;
this.dx *= -1;
}
if (this.x < max_left) {
this.x += max_left - this.x;
this.dx *= -1;
}
if ((distance(this.x, this.y, mouse.x, mouse.y)) < circles[this.index].radius) {
if (!this.mouseInside) {
this.mouseInside = true
console.log(`mouse enter circele at ${this.index}`)
}
}
else if (this.mouseInside) {
this.mouseInside = false
console.log(`mouse leave circele at ${this.index}`)
}
},
init: function (callback) {
var url = "https://t4.ftcdn.net/jpg/02/26/96/25/240_F_226962583_DzHr45pyYPdmwnjDoqz6IG7Js9AT05J4.jpg";
makeCircleImage(this.radius, url, function (img) {
this.image = img;
callback();
}.bind(this));
}
};
//Animate canvas
function animate() {
c.clearRect(0, 0, window.innerWidth, window.innerHeight);
circles.forEach(function (circle) {
circle.update();
});
circles.forEach(function (circle) {
circle.draw();
});
requestAnimationFrame(animate);
}
//Init canvas
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//init circle objects
var circles = [
new Circle(10, 100, 50, 0),
new Circle(10, 200, 30, 1),
new Circle(10, 300, 50, 2),
new Circle(10, 400, 50, 3),
new Circle(10, 500, 50, 4)
];
var ready = 0;
circles.forEach(function (circle) {
circle.init(oncircledone);
});
function oncircledone() {
if (++ready === circles.length) {
animate()
}
}
<canvas id="ctx"></canvas>
Ambiguities
It is not clear what you need in regard to circles and some point (in this answer point is a substitute for mouse and only requires that it have the properties x and y to be valid ).
The lack of information in your question concerns the facts
that many circles can be under the point at the same time.
and that more than one circle can move from under to out or out to under the point per frame.
the wording of the question suggest you are after just one circle which conflicts with the above 2 concerns.
Assumptions
I will assume that the interaction with the circles are more than just a simple on under event like interaction. That they may include animation related behaviors that are triggered by the state related to the point.
I assume that the visual order of the circles will determine how you select circles of interest.
That all circles per frame that meet the required conditions and can be accessed quickly.
That performance is important as you wish to have many circles that interact with a point.
That there is only one point (mouse, touch, other source) per frame that interacts with the circles
There is no requirement for circle circle interaction
Solution
The example below covers the above assumptions and resolves any ambiguities in the question. It is designed to be efficient and flexible.
The circles are stored in an array that has had its properties extended called circles
Rendering and state sets
The function circles.updateDraw(point) updates and draws all the circles. The argument point is a point to check the circle against. It defaults to the mouse.
All circles are drawn with an outline. Circles under the point (eg mouse) are filled with green, Circles just moved to under the point (eg onMouseOver) are filled with yellow, circle that have just move out from under are filled with red.
There are 3 arrays as properties of circles that contain circles as define...
circles.under All circles under the point
circles.outFromUnder All circles just out from under the point
circles.newUnder All circles new to under the point
These array are populated by the function circles.updateDraw(point)
Query all circles point state
Circles also have 3 functions that refer to the above arrays as set the default set is circles.under.
The functions are..
circles.firstInSet(set) Returns the first circle (The visual bottom most) in set or undefined
circles.lastInSet(set) Returns the last circle (The visual top most) in set or undefined
circles.closestInSet(set) Returns the closest circle to the point in set or undefined
For example to get the visual top most circle just under the mouse you would call circles.lastInSet(circles.newUnder) or to get the circle closest to the mouse from all circles under the mouse you would call circles.closestInSet(circles.newUnder) (or as it defaults to set under call circles.closestInSet() )
Circle additional states
Each Circle has some additional properties.
Circle.distSqr is the square of the distance from the point
Circle.rSqr is the square of the radius calculated when constructed.
Circle.underCount This value can be used to apply animations to the circle based on its relative state to the point.
If positive is the number of frames plus 1, the circle is under the point.
If this value is 1 then the circle is just moved from not under to under.
If this value is 0 the it has just moved out from under the point.
If negative this value is the number of frames the circle is not under the point
Running Demo
Use mouse to move over circles.
The circle closest and under the mouse is filled with white with alpha = 0.5
addEventListener('mousemove', event => {
mouse.x = event.clientX;
mouse.y = event.clientY;
});
Math.TAU = Math.PI * 2;
Math.rand = (min, max) => Math.random() * (max - min) + min;
const CIRCLE_RADIUS = 50;
const UNDER_STYLE = "#0A0";
const NEW_UNDER_STYLE = "#FF0";
const OUT_STYLE = "#F00";
const CIRCLE_STYLE = "#000";
const CIRCLE_LINE_WIDTH = 1.5;
const CIRCLE_COUNT = 100;
const CIRCLE_CLOSEST = "#FFF";
const ctx = canvas.getContext('2d');
const mouse = {x: 0, y: 0};
requestAnimationFrame(() => {
sizeCanvas();
var i = CIRCLE_COUNT;
while (i--) {
const r = Math.rand(CIRCLE_RADIUS / 3, CIRCLE_RADIUS);
circles.push(new Circle(
Math.rand(r, canvas.width - r),
Math.rand(r, canvas.height - r),
Math.rand(-1, 1),
Math.rand(-1, 1),
r
));
}
animate()
});
function animate() {
sizeCanvas();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
circles.updateDraw();
const c = circles.closestInSet(circles.under);
if(c) {
ctx.globalAlpha = 0.5;
ctx.beginPath();
ctx.fillStyle = CIRCLE_CLOSEST;
c.draw();
ctx.fill();
ctx.globalAlpha = 1;
}
requestAnimationFrame(animate);
}
function sizeCanvas() {
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
}
}
function Circle( x, y, dx = 0, dy = 0, radius = CIRCLE_RADIUS) {
this.x = x + radius;
this.y = y + radius;
this.dx = dx;
this.dy = dy;
this.radius = radius;
this.rSqr = radius * radius; // radius squared
this.underCount = 0; // counts frames under point
}
Circle.prototype = {
draw() {
ctx.moveTo(this.x + this.radius, this.y);
ctx.arc(this.x, this.y, this.radius, 0, Math.TAU);
},
update() {
this.x += this.dx;
this.y += this.dy;
if (this.x >= canvas.width - this.radius) {
this.x += (canvas.width - this.radius) - this.x;
this.dx = -Math.abs(this.dx);
} else if (this.x < this.radius) {
this.x += this.radius - this.x;
this.dx = Math.abs(this.dx);
}
if (this.y >= canvas.height - this.radius) {
this.y += (canvas.height - this.radius) - this.y;
this.dy = -Math.abs(this.dx);
} else if (this.y < this.radius) {
this.y += this.radius - this.y;
this.dy = Math.abs(this.dy);
}
},
isUnder(point = mouse) {
this.distSqr = (this.x - point.x) ** 2 + (this.y - point.y) ** 2; // distance squared
return this.distSqr < this.rSqr;
}
};
const circles = Object.assign([], {
under: [],
outFromUnder: [],
newUnder: [],
firstInSet(set = this.under) { return set[0] },
lastInSet(set = this.under) { return set[set.length - 1] },
closestInSet(set = this.under) {
var minDist = Infinity, closest;
if (set.length <= 1) { return set[0] }
for (const circle of set) {
if (circle.distSqr < minDist) {
minDist = (closest = circle).distSqr;
}
}
return closest;
},
updateDraw(point) {
this.under.length = this.newUnder.length = this.outFromUnder.length = 0;
ctx.strokeStyle = CIRCLE_STYLE;
ctx.lineWidth = CIRCLE_LINE_WIDTH;
ctx.beginPath();
for(const circle of this) {
circle.update();
if (circle.isUnder(point)) {
if (circle.underCount <= 0) {
circle.underCount = 1;
this.newUnder.push(circle);
} else { circle.underCount ++ }
this.under.push(circle);
} else if (circle.underCount > 0) {
circle.underCount = 0;
this.outFromUnder.push(circle);
} else {
circle.underCount --;
}
circle.draw();
}
ctx.stroke();
ctx.globalAlpha = 0.75;
ctx.beginPath();
ctx.fillStyle = UNDER_STYLE;
for (const circle of this.under) {
if (circle.underCount > 1) { circle.draw() }
}
ctx.fill();
ctx.beginPath();
ctx.fillStyle = OUT_STYLE;
for (const circle of this.outFromUnder) { circle.draw() }
ctx.fill();
ctx.beginPath();
ctx.fillStyle = NEW_UNDER_STYLE;
for (const circle of this.newUnder) { circle.draw() }
ctx.fill();
ctx.globalAlpha = 1;
}
});
#canvas {
position: absolute;
top: 0px;
left: 0px;
background: #6AF;
}
<canvas id="canvas"></canvas>
Well, the mouse is moving and you can simply create a Set which will contain circle objects that will store the circle(s) you are in:
let circleOfTrust = new Set();
//At the initialization you need to add any circles your point is currently in
and then at the loop:
circles.forEach(function( circle ) {
circleOfTrust[circle.update(circleOfTrust.has(circle)) ? "add" : "delete"](circle);
});
if (circleOfTrust.size() === 0) {
//point is outside the circles
} else {
//point is inside the circles in the set
}
and the update:
update: function (isInside) {
var
max_right = canvas.width + this.radius,
max_left = this.radius * -1;
this.x += this.dx;
if( this.x > max_right ) {
this.x += max_right - this.x;
this.dx *= -1;
}
if( this.x < max_left ) {
this.x += max_left - this.x;
this.dx *= -1;
}
return distance(circles[this.index].x, circles[this.index].y, mouse.x, mouse.y)) < circles[this.index].radius;
},
I would propose the following:
Keep a stack of figures with the order of how they were created (or any other meaningful order). This is needed to detect moves over overlapping figures.
Implement a function/method that iterates the stack and determines if the cursor is inside any of the figures.
Remember the last state, on state transition inside->ouside triggers an event.
function FiguresCollection(canvas, callback)
{
var buffer = [];
var lastHitFigure = null;
var addFigure = function(figure)
{
buffer.push(figure);
}
var onMouseMove = function(e)
{
var currentHit = null;
// iterating from the other end, recently added figures are overlapping previous ones
for (var i= buffer.length-1;i>=0;i--)
{
if (distance(e.offsetX, e.offsetY, buffer[i].x, buffer[i].y) <= buffer[i].radius) {
// the cursor is inside Figure i
// if it come from another figure
if (lastHitFigure !== i)
{
console.log("The cursor had left figure ", lastHitFigure, " and entered ",i);
callback(buffer[i]);
}
lastHitFigure = i;
currentHit = i;
break; // we do not care about figures potentially underneath
}
}
if (lastHitFigure !== null && currentHit == null)
{
console.log("the cursor had left Figure", lastHitFigure, " and is not over any other ");
lastHitFigure = null;
callback(buffer[lastHitFigure]);
}
}
}
canvas.addEventListener("mousemove", onMouseMove);
this.addFigure = addFigure;
}
Now use it:
var col = new FiguresCollection(canvas, c=> console.log("The cursor had left, ", c) );
for(let i in circles)
{
c.addFigure(circles[i]);
}
// I hope I got the code right. I haven't tested it. Please point out any issues or errors.

How to draw canvas trailing line with opacity

I'm attempting to draw the rotating line in this canvas animation with trailing opacity but it's not working. I've seen this effect with rectangles and arcs but never with a line, so I'm not sure what I need to add.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
} else {
timer = 0;
}
requestAnimationFrame(sonar);
}
}
sonar();
jsbin example
Here are two ways to do this: with a gradient and by adding translucent lines.
Sidenote, you should try and only redraw what you need to redraw. I separated the canvases and put one on top of the other so that we don't redraw the grid all the time.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function trail() {
var canvas = document.getElementById('trail');
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, innerWidth, innerHeight);
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.arc(cx,cy,cy,radians(timer-30),radians(timer));
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
var gradient = ctx.createLinearGradient(
cx+cy*Math.cos(radians(timer)), cy+cy*Math.sin(radians(timer)),
cx+cy*0.9*Math.cos(radians(timer-30)), cy+cy*0.9*Math.sin(radians(timer-30)));
gradient.addColorStop(0,'limegreen');
gradient.addColorStop(1,'transparent');
ctx.strokeStyle='transparent';
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
var fade = 10;
for(var i =0;i<fade;i++)
{
ctx.moveTo(cx, cy);
ctx.lineTo(cx+cy*Math.cos(radians(180+timer-i*1.3)),cy+cy*Math.sin(radians(180+timer-i*1.3)));
ctx.strokeStyle ="rgba(50,205,50,0.1)";
ctx.lineWidth=5;
ctx.closePath();
ctx.stroke();
}
} else {
timer = 0;
}
requestAnimationFrame(trail);
}
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
}
}
sonar();
trail();
canvas{
position: absolute;
}
<canvas id=sonar></canvas>
<canvas id=trail></canvas>
The problem is that to get this effect, you need to draw a triangle with a gradient along an arc, and you can't do that in a canvas. Gradients must be linear or radial.
The other option is to have an inner loop run each time you want to draw the sweeper, and go backwards from your sweeper line, drawing with slightly less opacity each time. But lets say you want your sweep to cover 15 degrees--obviously, if you have a 100% opacity line at d and a 5% opacity line at d - 15, that doesn't do the trick. So start filling in more lines, and more lines...you will have to draw so many lines to make it seem filled your performance would probably suffer.
My suggestion--you shouldn't have to redraw that on every frame. I would just make a PNG that looks like you want it to, and then place it and just rotate it around the center on each frame. No need to redraw it all the time then. That will be much faster than drawing a bunch of lines.
Canvas stack trails.
Below is a quick demo of how to use a stack of canvases to create a trailing effect.
You have a normal on screen canvas (this FX will not effect it) and then a stack of canvases for the trail FX. Each frame you move to the next canvas in the stack, first slightly clearing it then drawing to it what you want to trail. Then you render that canvas and the one just above it to the canvas.
A point to keep in mind is that the trails can also have a hugh range of FX, like blurring (just render each frame stack on itself slightly offset each time you render to it), zoom in and out trails. Trails on top or trails under. You can change the trail distance and much more.
It is overkill but over kill is fun.
The slider above the demo controls the trail length. Also the code need babel because I dont have time to write it for ES5.
Top slider is trail amount.One under that is trail distance. Trail dist does not transition well. Sorry about that.
//==============================================================================
// helper function
function $(query,q1){
if(q1 !== undefined){
if(typeof query === "string"){
var e = document.createElement(query);
if(typeof q1 !== "string"){
for(var i in q1){
e[i] = q1[i];
}
}else{
e.id = q1;
}
return e;
}
return [...query.querySelectorAll(q1)];
}
return [...document.querySelectorAll(query)];
}
function $$(element,e1){
if(e1 !== undefined){
if(typeof element === "string"){
$(element)[0].appendChild(e1);
return e1;
}
element.appendChild(e1);
return e1;
}
document.body.appendChild(element);
return element;
}
function $E(element,types,listener){
if(typeof types === "string"){
types = types.split(",");
}
element = $(element)[0];
types.forEach(t=>{
element.addEventListener(t,listener)
});
return element;
}
function R(I){
if(I === undefined){
return Math.random();
}
return Math.floor(Math.random()*I);
}
//==============================================================================
//==============================================================================
// answer code
// canvas size
const size = 512;
const trailDist = 10; // There is this many canvases so be careful
var trailDistCurrent = 10; // distance between trails
var clearAll = false;
// create a range slider for trail fade
$$($("input",{type:"range",width : size, min:0, max:100, step:0.1, value:50, id:"trail-amount",title:"Trail amount"}));
$("#trail-amount")[0].style.width = size + "px";
$E("#trail-amount","change,mousemove",function(e){fadeAmount = Math.pow(this.value / 100,2);});
// create a range slider trail distance
$$($("input",{type:"range",width : size, min:2, max:trailDist , step:1, value:trailDist , id:"trail-dist",title:"Trail seperation"}));
$("#trail-dist")[0].style.width = size + "px";
$E("#trail-dist","change,mousemove", function(e){
if(this.value !== trailDistCurrent){
trailDistCurrent= this.value;
clearAll = true;
}
});
$$($("br","")) // put canvas under the slider
// Main canvas
var canvas;
$$(canvas = $("canvas",{width:size,height:size})); // Not jquery. Just creates a canvas
// and adds canvas to the document
var ctx = canvas.getContext("2d");
// Trailing canvas
var trailCanvases=[];
var i =0; // create trail canvas
while(i++ < trailDist){trailCanvases.push($("canvas",{width:size,height:size}));}
var ctxT = trailCanvases.map(c=>c.getContext("2d")); // get context
var topCanvas = 0;
var fadeAmount = 0.5;
// Draw a shape
function drawShape(ctx,shape){
ctx.lineWidth = shape.width;
ctx.lineJoin = "round";
ctx.strokeStyle = shape.color;
ctx.setTransform(shape.scale,0,0,shape.scale,shape.x,shape.y);
ctx.rotate(shape.rot);
ctx.beginPath();
var i = 0;
ctx.moveTo(shape.shape[i++],shape.shape[i++]);
while(i < shape.shape.length){
ctx.lineTo(shape.shape[i++],shape.shape[i++]);
}
ctx.stroke();
}
// Create some random shapes
var shapes = (function(){
function createRandomShape(){
var s = [];
var len = Math.floor(Math.random()*5 +4)*2;
while(len--){
s[s.length] = (R() + R()) * 20 * (R() < 0.5 ? -1 : 1);
}
return s;
}
var ss = [];
var i = 10;
while(i--){
ss[ss.length] = createRandomShape();
}
ss[ss.length] = [0,0,300,0]; // create single line
return ss;
})();
// Create some random poits to move the shapes
var points = (function(){
function point(){
return {
color : "hsl("+R(360)+",100%,50%)",
shape : shapes[R(shapes.length)],
width : R(4)+1,
x : R(size),
y : R(size),
scaleMax : R()*0.2 + 1,
scale : 1,
s : 0,
rot : R()*Math.PI * 2,
dr : R()*0.2 -0.1,
dx : R()*2 - 1,
dy : R()*2 - 1,
ds : R() *0.02 + 0.01,
}
}
var line = shapes.pop();
var ss = [];
var i = 5;
while(i--){
ss[ss.length] = point();
}
var s = ss.pop();
s.color = "#0F0";
s.x = s.y = size /2;
s.dx = s.dy = s.ds = 0;
s.scaleMax = 0.5;
s.dr = 0.02;
s.shape = line;
s.width = 6;
ss.push(s);
return ss;
})();
var frameCount = 0; // used to do increamental fades for long trails
function update(){
// to fix the trail distance problem when fade is low and distance high
if(clearAll){
ctxT.forEach(c=>{
c.setTransform(1,0,0,1,0,0);
c.clearRect(0,0,size,size);
});
clearAll = false;
}
frameCount += 1;
// get the next canvas that the shapes are drawn to.
topCanvas += 1;
topCanvas %= trailDistCurrent;
var ctxTop = ctxT[topCanvas];
// clear the main canvas
ctx.setTransform(1,0,0,1,0,0); // reset transforms
// Fade the trail canvas
ctxTop.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,size,size); // clear main canvas
// slowly blendout trailing layer
if(fadeAmount < 0.1){ // fading much less than this leaves perminant trails
// so at low levels just reduce how often the fade is done
if(((Math.floor(frameCount/trailDistCurrent)+topCanvas) % Math.ceil(1 / (fadeAmount * 10))) === 0 ){
ctxTop.globalAlpha = 0.1;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
}else{
ctxTop.globalAlpha = fadeAmount;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
ctxTop.globalCompositeOperation = "source-over";
ctxTop.globalAlpha = 1;
// draw shapes
for(var i = 0; i < points.length; i ++){
var p = points[i];
p.x += p.dx; // move the point
p.y += p.dy;
p.rot += p.dr;
p.s += p.ds;
p.dr += Math.sin(p.s) * 0.001;
p.scale = Math.sin(p.s) * p.scaleMax+1;
p.x = ((p.x % size) + size) % size;
p.y = ((p.y % size) + size) % size;
drawShape(ctxTop,p); // draw trailing layer (middle)
}
// draw the trail the most distance from the current position
ctx.drawImage(trailCanvases[(topCanvas + 1)%trailDistCurrent],0,0);
// do it all again.
requestAnimationFrame(update);
}
update();

Making mouse position (0, 0) the center of the canvas

So I've recently been messing around with html5 canvas and trying to figure out how to make a particle system. While it does work I want to make vx = mouse coordinates(specifically x) but they start from the center.
What I mean by this is basically if the cursor is in the center of the canvas then vx would be equal to 0 and if the cursor is to the right from the center of the canvas it have positive mouse coordinates (and if the cursor is to the left from the center of canvas it have negative mouse coordinates).
Once that is accomplished I would just do p.speed += p.vx
Here is my javascript:
window.onload = function(){
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var W = window.innerWidth, H = window.innerHeight;
canvas.width = W;
canvas.height = H;
var particles = [];
var particle_count = 100;
for(var i = 0; i < particle_count; i++)
{
particles.push(new particle());
}
function particle()
{
this.vx = -1 + Math.random() * 2;
this.speed = {x: 0, y: -15+Math.random()*10};
this.location = {x: W/2, y: H/2};
this.radius = 5+Math.random()*10;
this.life = 20+Math.random()*10;
this.remaining_life = this.life;
this.r = Math.round(Math.random()*255);
this.g = Math.round(Math.random()*55);
this.b = Math.round(Math.random()*5);
}
function draw()
{
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "black";
ctx.fillRect(0, 0, W, H);
ctx.globalCompositeOperation = "lighter";
for(var i = 0; i < particles.length; i++)
{
var p = particles[i];
ctx.beginPath();
p.opacity = Math.round(p.remaining_life/p.life*100)/100
var gradient = ctx.createRadialGradient(
p.location.x, p.location.y, 0, p.location.x, p.location.y, p.radius);
gradient.addColorStop(0, "rgba("+p.r+", "+p.g+", "+p.b+", "+p.opacity+")");
gradient.addColorStop(0.5, "rgba("+p.r+", "+p.g+", "+p.b+", "+p.opacity+")");
gradient.addColorStop(1, "rgba("+p.r+", "+p.g+", "+p.b+", 0)");
ctx.fillStyle = gradient;
ctx.arc(p.location.x, p.location.y, p.radius, Math.PI*2, false);
ctx.fill();
p.remaining_life--;
p.radius--;
p.location.x += p.speed.x += p.vx;
p.location.y += p.speed.y;
if(p.remaining_life < 0 || p.radius < 0) {
particles[i] = new particle();
}
}
}
setInterval(draw, 33); }
Here is my codepen.
You need to translate context to move origin to center.
You also need to reverse the translation on the mouse coordinates as they will still be relative to the upper-left corner (assuming the coordinates are corrected to be relative to canvas).
Example:
var cx = canvas.width * 0.5;
var cy = canvas.height * 0.5;
ctx.translate(cx, cy); // translate globally once
For each mouse coordinate compensate with the translated position:
var pos = getMousePosition(evt); // see below
var x = pos.x - cx;
var y = pos.y - cy;
To adjust mouse position:
function getMousePosition(evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
}
}

Categories