Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an afterimage for some objects in JavaScript

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.

like image 218
KoderM Avatar asked Dec 29 '25 06:12

KoderM


1 Answers

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:

fps meter rendered on top of a particle simulation of fireworks with trails (faded background)

The the fireworks have trails, but the text is clear (witout blur/trails)

like image 130
George Profenza Avatar answered Dec 31 '25 18:12

George Profenza