Related
I'm working on a firework display with the p5.js library (although I doubt this will effect answers). My current program uses the p5.js background function with can create an afterimage for the entire project. You can view the project here: https://editor.p5js.org/KoderM/sketches/WsluEg00h
But you can also view the code here:
let particles, FPS;
function setup() {
createCanvas(1000, 800);
background(220);
particles = [];
FPS = new FPSMonitor(50, 50);
}
function draw() {
colorMode(RGB);
background(0, 0, 0, 25);
for(let p in particles){
if(particles[p].isDead()){
particles.splice(p, 1);
continue;
}
particles[p].update();
}
FPS.update();
}
function mousePressed(){
particles.push(new firework(mouseX, mouseY, random(0, 255)));
particles[particles.length-1].velocity = p5.Vector.random2D();
particles[particles.length-1].velocity.mult(random(0.5, 10));
}
class FPSMonitor {
constructor(x, y){
this.x = x;
this.y = y;
this.size = 100;
this.delay = millis();
this.mouse = [0, 0];
}
update(){
this.checkMouse();
if(this.mouse[0] !== 0){
this.x = mouseX - this.mouse[0];
this.y = mouseY - this.mouse[1];
}
textAlign(LEFT, TOP);
textSize(this.size/6);
rectMode(CORNER);
strokeWeight(3);
stroke("red");
fill("white");
rect(this.x, this.y, this.size*1.2, this.size);
strokeWeight(4);
stroke("black");
text("FPS: " + round(1/(millis()-this.delay)*1000, 3), this.x+5, this.y+5);
text("Average FPS:", this.x+5, this.y+25);
text(round(frameCount/millis()*1000, 3), this.x+5, this.y+48);
text("MS: " + round(millis()-this.delay), this.x+5, this.y+72);
this.delay = millis();
}
checkMouse(){
if(mouseIsPressed && this.mouse[0] !== 0){
return;
}
if(this.x < mouseX && (this.x + this.size) > mouseX &&
this.y < mouseY && (this.y + this.size) > mouseY && mouseIsPressed){
if(this.mouse[0] == 0){
this.mouse = [ mouseX - this.x, mouseY - this.y ]
}
return;
}
this.mouse = [0, 0];
}
}
Particle.js:
class particle {
constructor(x, y, hue, gravity, life, weight, renderFunction){
if(!hue){ throw new TypeError(this + " : hue is not defined") }
this.defaults = {
x: 0,
y: 0,
gravity: createVector(0, 0),
life: 100,
weight: 1,
renderFunction: (self) => {colorMode(HSB);strokeWeight(2);stroke(this.hue, 255, 255, this.life/this.maxLife);point(this.position.x, this.position.y)}
}
this.position = x && y ? createVector(x, y) : createVector(this.defaults.x, this.defaults.y);
this.gravity = gravity || this.defaults.gravity;
this.life = life || this.defaults.life;
this.maxLife = this.life;
this.acceleration = createVector(0, 0);
this.velocity = createVector(0, 0);
this.weight = weight || this.defaults.weight;
this.renderFunction = renderFunction || this.defaults.renderFunction;
this.hue = hue;
this.otherInfo = {
mouseAtStart: createVector(mouseX, mouseY),
}
}
isDead(){
return this.life < 0;
}
applyForce(force){
this.acceleration.add(force);
}
update(){
this.life--;
this.acceleration.add(this.gravity);
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.velocity.mult(this.weight*0.96>1?0.96:this.weight*0.96);
this.acceleration.mult(0.1);
this.renderFunction(this);
}
}
And finally, firework.js:
class firework {
constructor(x, y, hue){
this.renderFunction = self => {
colorMode(HSB);
strokeWeight(3);
stroke(self.hue, 255, 255, (self.life+self.maxLife*0.5)/self.maxLife)
//line(self.otherInfo.mouseAtStart.x, self.otherInfo.mouseAtStart.y, self.position.x, self.position.y);
point(self.position.x, self.position.y);
};
this.explodeRenderFunction = self => {
colorMode(HSB);
strokeWeight(3);
stroke(self.hue, 255, 255, self.life/self.maxLife)
//line(self.otherInfo.mouseAtStart.x, self.otherInfo.mouseAtStart.y, self.position.x, self.position.y);
point(self.position.x, self.position.y);
}
this.particle = new particle(x, y, hue, createVector(0, 0.1), height/53.3 * 4, 1, this.renderFunction);
this.particle.applyForce(createVector(random(-3, 3), random(height/-53.3, height/-43.3)));
this.explodeParticles = [];
this.exploded = false;
this.hue = hue;
}
update(){
this.particle.update();
if(this.particle.isDead() && !this.exploded){
this.particle.renderFunction = (self) => {};
this.exploded = true;
for(let p = 0; p < 500; p++){
this.explodeParticles.push(new particle(this.particle.position.x, this.particle.position.y, this.hue, createVector(0, 0.1), 100, 1, this.explodeRenderFunction));
this.explodeParticles[this.explodeParticles.length-1].velocity = p5.Vector.random2D();
this.explodeParticles[this.explodeParticles.length-1].velocity.mult(random(0.5, 10));
}
}
if(this.exploded){
for(let p in this.explodeParticles){
if(this.explodeParticles[p].isDead()){
this.explodeParticles.splice(p, 1);
continue;
}
this.explodeParticles[p].update();
}
}
}
isDead(){
return this.explodeParticles.length == 0 && this.exploded;
}
}
The fireworks DON'T look at all like fire works without the trail the afterimage provides, but I've also implemented an FPS monitor I created, which also blurs because of the background function (this effect is unwanted.)
A bit more information on the background function:
I'm using the syntax: background(v1, v2, v3, [a]) v1, v2, and v3 are HSB variables. The optional variable [a] is defined as: Opacity of the background relative to current color range (default is 0-255)
The full background website: https://p5js.org/reference/#/p5/background
THE QUESTION
How do I have the fireworks look the same, WITHOUT having other things in the canvas be effected? For example, the FPS monitor in the project can move by dragging your mouse, but it also has that afterimage effect that comes from the background function. I want the fireworks to stay the same, but anything else that renders needs to be "not" blurry.
Really appreciate any help.
You could use separate "layers" in p5.js using createGraphics().
For example, the fireworks class could hold it's p5.Graphics instance which it can use to render the effect into, then in the main sketch's draw() you'd call image(), passing the p5.Graphics instance (as if it was an image) to display into the main p5.js canvas.
(Off-topic, you could look into object pooling to reset/reuse "dead" particles
instead of deleting/re-allocating new ones)
Update
Seems to work, here's what an approach to what I meant:
let particles, FPS;
// independent layers to render graphics into
let particlesLayer;
let fpsLayer;
function setup() {
createCanvas(1000, 800);
background(0);
particles = [];
FPS = new FPSMonitor(50, 50);
// particles will take up the whole sketch
particlesLayer = createGraphics(width, height);
// we can get away with a smaller frame buffer for the FPS meter
fpsLayer = createGraphics(256, 216);
}
function draw() {
particlesLayer.colorMode(RGB);
particlesLayer.background(0, 0, 0, 25);
for(let p in particles){
if(particles[p].isDead()){
particles.splice(p, 1);
continue;
}
particles[p].update();
}
// render the fireworks layers into the main sketch
image(particlesLayer, 0, 0);
// pass the layer to render the fps meter into and display it
FPS.update(fpsLayer);
}
function mousePressed(){
// pass the particles layer to each firework instance to render into
particles.push(new firework(mouseX, mouseY, random(0, 255), particlesLayer));
particles[particles.length-1].velocity = p5.Vector.random2D();
particles[particles.length-1].velocity.mult(random(0.5, 10));
}
function mouseDragged(){
FPS.x = mouseX - FPS.size * 0.5;
FPS.y = mouseY - FPS.size * 0.5;
}
class FPSMonitor {
constructor(x, y){
this.x = x;
this.y = y;
this.size = 100;
this.delay = millis();
}
update(g){
g.textAlign(LEFT, TOP);
g.textSize(this.size/6);
g.rectMode(CORNER);
g.strokeWeight(3);
g.stroke("red");
g.fill("white");
g.rect(0, 0, this.size * 1.2, this.size);
g.noStroke();
g.fill(0);
g.text("FPS: " + round(1/(millis()-this.delay)*1000, 3), 5, 5);
g.text("Average FPS:", 5, 25);
g.text(round(frameCount/millis()*1000, 3), 5, 48);
g.text("MS: " + round(millis()-this.delay), 5, 72);
this.delay = millis();
// render the graphics
image(g, this.x, this.y);
}
}
class particle {
constructor(x, y, hue, gravity, life, weight, renderFunction){
if(!hue){ throw new TypeError(this + " : hue is not defined") }
this.defaults = {
x: 0,
y: 0,
gravity: createVector(0, 0),
life: 100,
weight: 1,
renderFunction: (self) => {colorMode(HSB);strokeWeight(2);stroke(this.hue, 255, 255, this.life/this.maxLife);point(this.position.x, this.position.y)}
}
this.position = x && y ? createVector(x, y) : createVector(this.defaults.x, this.defaults.y);
this.gravity = gravity || this.defaults.gravity;
this.life = life || this.defaults.life;
this.maxLife = this.life;
this.acceleration = createVector(0, 0);
this.velocity = createVector(0, 0);
this.weight = weight || this.defaults.weight;
this.renderFunction = renderFunction || this.defaults.renderFunction;
this.hue = hue;
this.otherInfo = {
mouseAtStart: createVector(mouseX, mouseY),
}
}
isDead(){
return this.life < 0;
}
applyForce(force){
this.acceleration.add(force);
}
update(){
this.life--;
this.acceleration.add(this.gravity);
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
this.velocity.mult(this.weight*0.96>1?0.96:this.weight*0.96);
this.acceleration.mult(0.1);
this.renderFunction(this);
}
}
class firework {
constructor(x, y, hue, graphicsLayer){
// store the reference to the same particle layers
this.g = graphicsLayer;
this.renderFunction = self => {
// use the layer to draw into, not the global p5.js graphics
this.g.colorMode(HSB);
this.g.strokeWeight(3);
this.g.stroke(self.hue, 255, 255, (self.life+self.maxLife*0.5)/self.maxLife)
//line(self.otherInfo.mouseAtStart.x, self.otherInfo.mouseAtStart.y, self.position.x, self.position.y);
this.g.point(self.position.x, self.position.y);
};
this.explodeRenderFunction = self => {
this.g.colorMode(HSB);
this.g.strokeWeight(3);
this.g.stroke(self.hue, 255, 255, self.life/self.maxLife)
//line(self.otherInfo.mouseAtStart.x, self.otherInfo.mouseAtStart.y, self.position.x, self.position.y);
this.g.point(self.position.x, self.position.y);
}
this.particle = new particle(x, y, hue, createVector(0, 0.1), height/53.3 * 4, 1, this.renderFunction);
this.particle.applyForce(createVector(random(-3, 3), random(height/-53.3, height/-43.3)));
this.explodeParticles = [];
this.exploded = false;
this.hue = hue;
}
update(){
this.particle.update();
if(this.particle.isDead() && !this.exploded){
this.particle.renderFunction = (self) => {};
this.exploded = true;
for(let p = 0; p < 500; p++){
this.explodeParticles.push(new particle(this.particle.position.x, this.particle.position.y, this.hue, createVector(0, 0.1), 100, 1, this.explodeRenderFunction));
this.explodeParticles[this.explodeParticles.length-1].velocity = p5.Vector.random2D();
this.explodeParticles[this.explodeParticles.length-1].velocity.mult(random(0.5, 10));
}
}
if(this.exploded){
for(let p in this.explodeParticles){
if(this.explodeParticles[p].isDead()){
this.explodeParticles.splice(p, 1);
continue;
}
this.explodeParticles[p].update();
}
}
}
isDead(){
return this.explodeParticles.length == 0 && this.exploded;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.min.js"></script>
And here's a screenshot:
The the fireworks have trails, but the text is clear (witout blur/trails)
I want to animate a rectangle on html canvas. when the user will click the canvas, the rectangle will start it's animation, and go to the clicked position. I used delta x and y to add and subtract pixels from the x and y position of the rectangle.
But the problem with this solution is, I can't find a way to make the rectangle animate in a straight path.
My code:
'use strict'
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const ratio = Math.ceil(window.devicePixelRatio)
let height = window.innerHeight
let width = window.innerWidth
canvas.height = height * ratio
canvas.width = width * ratio
canvas.style.height = `${height}px`
canvas.style.width = `${width}px`
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
let position = {
x: 0,
y: 0,
deltaX: 0,
deltaY: 0,
size: 20
}
let move = {
x: 0,
y: 0,
}
function animate() {
if (position.x === move.x && position.y === move.y) {
cancelAnimationFrame()
}
if (position.x !== move.x) {
ctx.fillRect(position.x, position.y, position.size, position.size)
position.x += position.deltaX
}
if (position.y !== move.y) {
ctx.fillRect(position.x, position.y, position.size, position.size)
position.y += position.deltaY
}
requestAnimationFrame(animate)
}
function moveTo(x, y) {
move.x = x
move.y = y
position.deltaX = position.x > x ? -1 : 1
position.deltaY = position.y > y ? -1 : 1
animate()
}
canvas.addEventListener('click', (event) => {
moveTo(event.clientX, event.clientY)
})
ctx.fillRect(position.x, position.y, position.size, position.size)
<canvas id="canvas">
</canvas>
If you click in the canvas the rectangle will start moving but it'll go in a weird path, I can't find a way to properly go straight at the clicked position.
see demo at Github page
Here is the sample Math from something I did a while ago...
If there is something you don't understand there ask
In your code it was moving "weird" because your delta values where always 1 or -1, no fractions, that limits the way the object can travel, instead we do our calculations using the angle.
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let player = {
x: 0, y: 0, size: 10,
delta: { x: 0, y: 0 },
move: { x: 0, y: 0 }
}
function animate() {
let a = player.x - player.move.x;
let b = player.y - player.move.y;
if (Math.sqrt( a*a + b*b ) < 2) {
player.delta = { x: 0, y: 0 }
}
player.x += player.delta.x
player.y += player.delta.y
ctx.fillRect(player.x, player.y, player.size, player.size)
requestAnimationFrame(animate)
}
function moveTo(x, y) {
player.move.x = x
player.move.y = y
let angle = Math.atan2(y - player.y, x - player.x)
player.delta.x = Math.cos(angle)
player.delta.y = Math.sin(angle)
}
canvas.addEventListener('click', (event) => {
moveTo(event.clientX, event.clientY)
})
animate()
<canvas id="canvas"></canvas>
I am trying to move the whole car object with its different body elements and their attributes, but all of the elements are moving in different speeds instead of the whole car moving in one speed.
Any help with this would be great.
I am also using a random number value for the speed.
Here is the code:
class Car {
constructor(p) {
//moving everything
this.direction = p.direction || 1;
//wheel constructor
this.x = p.x || 130;
this.y = p.y || 160;
this.wheelSize = p.wheelSize || 15;
this.WHcolour = p.WHcolour || "black";
//body constructor
this.carW = p.carW || 130;
this.carH = p.carH || 50;
this.Ccolour = p.Ccolour || "red";
//window constructor
this.windowW = p.windowW || 25;
this.windowH = p.windowH || 15;
this.Wcolour = p.Wcolour || "blue";
//car number
this.carNum = p.carNum || 1;
}
draw() {
//draw wheel 1
cxt.beginPath();
cxt.arc(this.x, this.y, this.wheelSize, 0, 2 * Math.PI);
cxt.fillStyle = this.WHcolour;
cxt.fill();
//draw wheel 2
cxt.beginPath();
cxt.arc(this.x * 1.8, this.y, this.wheelSize, 0, 2 * Math.PI);
cxt.fillStyle = this.WHcolour;
cxt.fill();
//draw body
cxt.beginPath();
cxt.rect(this.x * 0.9, this.y * 0.65, this.carW, this.carH);
cxt.fillStyle = this.Ccolour
cxt.fill();
//draw window1
cxt.beginPath();
cxt.rect(this.x * 1.615, this.y * 0.69, this.windowW, this.windowH);
cxt.fillStyle = this.Wcolour;
cxt.fill();
//draw window 2
cxt.beginPath();
cxt.rect(this.x * 1.0461, this.y * 0.69, this.windowW * 2.4, this.windowH);
cxt.fillStyle = this.Wcolour;
cxt.fill();
}
move() {
//velocity increasment
//wheels
this.x += this.direction;
//body
}
}
function createCar() {
while (cars.length < 1) {
let c = new Car({
//wheels
WHcolour: "black",
x: 130, y: 160, wheelSize: 15,
//body
carW: 130, carH: 50, Ccolour: "yellow",
//windows
windowW: 25, windowH: 15, Wcolour: "blue",
//car number
carNum: 1,
});
cars.push(c)
}
}
You can not multiply on the same axis of your movement, you can make the part relative to each other by adding an offset, as you build more complex object multiplication can be used for something that you will launch from the car, like a rocket that will accelerate relative to the position of the car.
Here is your code working
class Car {
constructor(p) {
//moving everything
this.direction = p.direction || 1;
//wheel constructor
this.x = p.x || 130;
this.y = p.y || 160;
this.wheelSize = p.wheelSize || 15;
this.WHcolour = p.WHcolour || "black";
//body constructor
this.carW = p.carW || 130;
this.carH = p.carH || 50;
this.Ccolour = p.Ccolour || "red";
//window constructor
this.windowW = p.windowW || 25;
this.windowH = p.windowH || 15;
this.Wcolour = p.Wcolour || "blue";
//car number
this.carNum = p.carNum || 1;
}
draw() {
//draw wheel 1
cxt.beginPath();
cxt.arc(this.x, this.y, this.wheelSize, 0, 2 * Math.PI);
cxt.fillStyle = this.WHcolour;
cxt.fill();
//draw wheel 2
cxt.beginPath();
cxt.arc(this.x + 90, this.y, this.wheelSize, 0, 2 * Math.PI);
cxt.fillStyle = this.WHcolour;
cxt.fill();
//draw body
cxt.beginPath();
cxt.rect(this.x - 20 , this.y * 0.65, this.carW, this.carH);
cxt.fillStyle = this.Ccolour
cxt.fill();
//draw window1
cxt.beginPath();
cxt.rect(this.x + 70, this.y * 0.69, this.windowW, this.windowH);
cxt.fillStyle = this.Wcolour;
cxt.fill();
//draw window 2
cxt.beginPath();
cxt.rect(this.x - 10, this.y * 0.69, this.windowW * 2.4, this.windowH);
cxt.fillStyle = this.Wcolour;
cxt.fill();
}
move() {
//velocity increasment
//wheels
this.x += this.direction;
//body
}
}
function createCar() {
while (cars.length < 1) {
let c = new Car({
//wheels
WHcolour: "black",
x: 130, y: 160, wheelSize: 15,
//body
carW: 130, carH: 50, Ccolour: "yellow",
//windows
windowW: 25, windowH: 15, Wcolour: "blue",
//car number
carNum: 1,
});
cars.push(c)
}
}
function move() {
cxt.clearRect(0,0, 800,800)
for (i = 0; i < cars.length; i++) {
cars[i].draw()
cars[i].move()
}
}
let canvas = document.createElement("canvas");
canvas.width = canvas.height = 800;
let cxt = canvas.getContext('2d');
document.body.appendChild(canvas);
var cars = []
createCar()
setInterval(move, 100);
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.
Currently attempting to make a physics simulation for elastic collisions of circles. I am having an issue where I do not know how to run the simulation with two circles interacting at the same time. I am not yet looking to create the interaction between the circles just to have them both running simultaneously. Any help is much appreciated. This is my first post so I apologize if I formatted something incorrectly.
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var loopTimer = false;
var ball = {
position: {
x: width / 2,
y: height / 2
},
velocity: {
x: 0,
y: 0
},
radius: 15, // 1px = 1cm
restitution: -1
};
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
var mouseDown = function(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
ball.position.x = mouse.x;
ball.position.y = mouse.y;
}
}
var mouseUp = function(event) {
if (event.which == 1) {
mouse.isDown = false;
ball.velocity.y = (ball.position.y - mouse.y) / 10;
ball.velocity.x = (ball.position.x - mouse.x) / 10;
}
}
var setup = function() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
loopTimer = setInterval(loop, frameDelay);
}
var loop = function() {
if (!mouse.isDown) {
ball.position.x += ball.velocity.x * frameRate * 100;
ball.position.y += ball.velocity.y * frameRate * 100;
}
if (ball.position.y > height - ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = height - ball.radius;
}
if (ball.position.x > width - ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = width - ball.radius;
}
if (ball.position.x < ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = ball.radius;
}
if (ball.position.y < ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = ball.radius;
}
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(ball.position.x, ball.position.y);
ctx.beginPath();
ctx.arc(0, 0, ball.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
if (mouse.isDown) {
ctx.beginPath();
ctx.moveTo(ball.position.x, ball.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
Here is how I would do it:
Instead of making the ball a kind of static object I made a constructor function (More about that here).
Then I made a ball array to store all the balls.
To make the dragging possible I store a seperate ball, which is not being moved by "physics" in the newBall variable. This ball is either invisible or is the ball currently being dragged.
In mouseDown() the newBall gets positioned under the cursor.
In mouseUp() it gets it's velocity and gets added to the array of animated balls. Also a new newBall gets created.
In loop() I loop two times through the array of animated balls. Once for the physics, once for the painting.
(Normally you would use two different methods with different tickRates to make animation more smooth, because physics calculation doesn't need to happen 60 times per second.
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var loopTimer = false;
function ball() {
this.position = {
x: width / 2,
y: height / 2
};
this.velocity = {
x: 0,
y: 0
};
this.radius = 15; // 1px = 1cm
this.restitution = -1
};
var balls = [];
var newBall = new ball();
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
var mouseDown = function(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
newBall.position.x = mouse.x;
newBall.position.y = mouse.y;
}
}
var mouseUp = function(event) {
if (event.which == 1) {
mouse.isDown = false;
newBall.velocity.y = (newBall.position.y - mouse.y) / 10;
newBall.velocity.x = (newBall.position.x - mouse.x) / 10;
balls.push(newBall);
newBall = new ball();
}
}
var setup = function() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
loopTimer = setInterval(loop, frameDelay);
}
var loop = function() {
for (var ball of balls) {
ball.position.x += ball.velocity.x * frameRate * 100;
ball.position.y += ball.velocity.y * frameRate * 100;
if (ball.position.y > height - ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = height - ball.radius;
}
if (ball.position.x > width - ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = width - ball.radius;
}
if (ball.position.x < ball.radius) {
ball.velocity.x *= ball.restitution;
ball.position.x = ball.radius;
}
if (ball.position.y < ball.radius) {
ball.velocity.y *= ball.restitution;
ball.position.y = ball.radius;
}
}
ctx.clearRect(0, 0, width, height);
for (var ball of balls) {
ctx.save();
ctx.translate(ball.position.x, ball.position.y);
ctx.beginPath();
ctx.arc(0, 0, ball.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
}
ctx.save();
ctx.translate(newBall.position.x, newBall.position.y);
ctx.beginPath();
ctx.arc(0, 0, newBall.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
if (mouse.isDown) {
ctx.beginPath();
ctx.moveTo(newBall.position.x, newBall.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
Now to get a bit more complex:
I added tickDelay and tickTimer to use them in a tickLoop
The ball constructor now has two methods:
show() draws the ball on the canvas
tick() does the pysics stuff (dt= deltaTime: time since last tick)
newBall is now null if the mouse isn't pressed
setup() initializes the width and height according to the <canvas> elements real size
tick() loops through the balls and calls .tick() tickDelay is in milliseconds so it gets divided by 1000
drawFrame() is your former loop() and does the drawing stuff
var width = 400;
var height = 400;
var canvas = ctx = false;
var frameRate = 1 / 60; // Seconds
var frameDelay = frameRate * 1000; // ms
var tickDelay = frameDelay * 2; //ticks 2 times slower than frames
var frameTimer;
var tickTimer;
function ball() {
this.position = {
x: width / 2,
y: height / 2
};
this.velocity = {
x: 0,
y: 0
};
this.radius = 15; // 1px = 1cm
this.restitution = -.99;
this.show = function() {
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
ctx.restore();
};
this.tick = function(dt) {
this.position.x += this.velocity.x * dt;
this.position.y += this.velocity.y * dt;
if (this.position.y > height - this.radius) {
this.velocity.y *= this.restitution;
this.position.y = height - this.radius;
}
if (this.position.x > width - this.radius) {
this.velocity.x *= this.restitution;
this.position.x = width - this.radius;
}
if (this.position.x < this.radius) {
this.velocity.x *= this.restitution;
this.position.x = this.radius;
}
if (this.position.y < this.radius) {
this.velocity.y *= this.restitution;
this.position.y = this.radius;
}
}
};
var balls = [];
var newBall;
var mouse = {
x: 0,
y: 0,
isDown: false
};
function getMousePosition(event) {
mouse.x = event.pageX - canvas.offsetLeft;
mouse.y = event.pageY - canvas.offsetTop;
}
function mouseDown(event) {
if (event.which == 1) {
getMousePosition(event);
mouse.isDown = true;
if (!newBall) newBall = new ball();
newBall.position.x = mouse.x;
newBall.position.y = mouse.y;
}
}
function mouseUp(event) {
if (event.which == 1) {
mouse.isDown = false;
newBall.velocity.y = (newBall.position.y - mouse.y);
newBall.velocity.x = (newBall.position.x - mouse.x);
balls.push(newBall);
newBall = null;
}
}
function setup() {
canvas = document.getElementById("canvas");
width = canvas.getBoundingClientRect().width;
height = canvas.getBoundingClientRect().height;
ctx = canvas.getContext("2d");
canvas.onmousemove = getMousePosition;
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
ctx.fillStyle = 'blue';
ctx.strokeStyle = '#000000';
requestAnimationFrame(drawFrame);
frameTimer = setInterval(drawFrame, frameDelay);
tickTimer = setInterval(tick, tickDelay);
}
function tick() {
for (var ball of balls) ball.tick(tickDelay * .001);
}
function drawFrame() {
ctx.clearRect(0, 0, width, height);
for (var ball of balls) ball.show();
if (newBall) newBall.show(ctx);
if (mouse.isDown && newBall) {
ctx.beginPath();
ctx.moveTo(newBall.position.x, newBall.position.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
ctx.closePath();
}
}
setup();
#canvas {
border: solid 1px #ccc;
}
<canvas id="canvas"></canvas>
A really simple way would to do exactly the same as you do now, but not initiate all functions as a variable. Change all the variables that are functions to just functions, and where you call them. At least the variable called ball. Then after that you could make two variables like this
ball1 = new ball();
ball2 = new ball();
Your script is kind of messy so hard for me to say if this will go through without any errors, but if it does, I am more than happy to help. This is not the very best solution if you only go for the way i presented now so please do not use this as you solution, but more as a way to get started. Also you will not really learn anything of it if we just gave you the answer
Edit:
Another thing to mark is that using setInterval for games and graphical projects may be a bad idea since JavaScript is single threaded. A better solution is using requestAnimationFrame()
It would look something like this
function mainLoop() {
update();
draw();
requestAnimationFrame(mainLoop);
}
// Start things off
requestAnimationFrame(mainLoop);