4

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.

KoderM
  • 382
  • 2
  • 15

1 Answers1

4

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)

George Profenza
  • 50,687
  • 19
  • 144
  • 218
  • This is doesn't seem to work, I tried it... Not sure what the problem is though. – KoderM May 10 '22 at 01:32
  • It might be because the image is going to render on the main canvas anyway. So any image rendered onto it will be whatever the original canvas had. Not quite sure this is the case though. – KoderM May 10 '22 at 03:52
  • @KoderM I've updated my answer to include a running example. (The FPS layer is optional: it could've been the global p5.js sketch graphics, but it might make it easier to understand the approach with the simpler FPS meter first) – George Profenza May 10 '22 at 21:17
  • This further proves my point, it's not working... – KoderM May 10 '22 at 23:04
  • 1
    Ha! :))) I missed a crucial detail of your question: dragging the FPS meter. The idea of using `p5.Graphics` as "layers" still stands. I've updated the code snippet above and the FPS meter can be dragged without leaving trails. I've simplified the dragging logic a bit to use less code and make use of the [`mouseDragged()`](https://p5js.org/reference/#/p5/mouseDragged). Nicely documented question and fun fireworks effect ! Have fun :) – George Profenza May 10 '22 at 23:24
  • 1
    No worries. Let me know if the code makes sense. It could still be cleaned up / improved further. The main point regarding your question was using different layers to render into so the trails of the fireworks don't interfere with the fps meter. (In this case the fireworks act as a background to clear the trails of the fireworks, but in other cases you will need to manually call `background(0);` on the main sketch's graphics to clear everything first). – George Profenza May 10 '22 at 23:46