-4

Why does the below canvas have a black background? Because of it, the text behind the canvas is not visible. I need it to be transparent to show the elements behind it and the background image of the body.

There is a similar question on Stack Overflow but the solution given for that question doesn't work correctly for me.

        "use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];

function setup() {
    canvas = document.getElementById("canvas");
    setSize(canvas);
    ctx = canvas.getContext("2d");
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);
    fireworks.push(new Firework(Math.random()*(width-200)+100));
    window.addEventListener("resize",windowResized);
    document.addEventListener("click",onClick);
}

setTimeout(setup,1);

function loop(){
    ctx.globalAlpha = 0.1;
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);
    ctx.globalAlpha = 1;

    for(let i=0; i<fireworks.length; i++){
        let done = fireworks[i].update();
        fireworks[i].draw();
        if(done) fireworks.splice(i, 1);
    }

    for(let i=0; i<particles.length; i++){
        particles[i].update();
        particles[i].draw();
        if(particles[i].lifetime>80) particles.splice(i,1);
    }

    if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
setInterval(loop, 1/60);
//setInterval(loop, 100/60);
class Particle{
    constructor(x, y, col){
        this.x = x;
        this.y = y;
        this.col = col;
        this.vel = randomVec(2);
        this.lifetime = 0;
    }

    update(){
        this.x += this.vel.x;
        this.y += this.vel.y;
        this.vel.y += 0.02;
        this.vel.x *= 0.99;
        this.vel.y *= 0.99;
        this.lifetime++;
    }

    draw(){
        ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
        ctx.fillStyle = this.col;
        ctx.fillRect(this.x, this.y, 2, 2);
    }
}

class Firework{
    constructor(x){
        this.x = x;
        this.y = height;
        this.isBlown = false;
        this.col = randomCol();
    }

    update(){
        this.y -= 3;
        if(this.y < 350-Math.sqrt(Math.random()*500)*40){
            this.isBlown = true;
            for(let i=0; i<60; i++){
                particles.push(new Particle(this.x, this.y, this.col))
            }
        }
        return this.isBlown;
        
    }

    draw(){
        ctx.globalAlpha = 1;
        ctx.fillStyle = this.col;
        ctx.fillRect(this.x, this.y, 2, 2);
    }
}

function randomCol(){
    var letter = '0123456789ABCDEF';
    var nums = [];

    for(var i=0; i<3; i++){
        nums[i] = Math.floor(Math.random()*256);
    }

    let brightest = 0;
    for(var i=0; i<3; i++){
        if(brightest<nums[i]) brightest = nums[i];
    }

    brightest /=255;
    for(var i=0; i<3; i++){
        nums[i] /= brightest;
    }

    let color = "#";
    for(var i=0; i<3; i++){
        color += letter[Math.floor(nums[i]/16)];
        color += letter[Math.floor(nums[i]%16)];
    }
    return color;
}

function randomVec(max){
    let dir = Math.random()*Math.PI*2;
    let spd = Math.random()*max;
    return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}

function setSize(canv){
    canv.style.width = (innerWidth) + "px";
    canv.style.height = (innerHeight) + "px";
    width = innerWidth;
    height = innerHeight;

    canv.width = innerWidth*window.devicePixelRatio;
    canv.height = innerHeight*window.devicePixelRatio;
    canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}

function onClick(e){
    fireworks.push(new Firework(e.clientX));
}

function windowResized(){
    setSize(canvas);
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);
}
    <canvas id="canvas"></canvas>
    <h1>Hello</h1>
Archit Gargi
  • 634
  • 1
  • 8
  • 24
  • 2
    `ctx.fillStyle = "#000000";` and then `ctx.fillRect(0, 0, width, height);` - what did you think this was going to do? – Jamiec Mar 28 '22 at 10:02
  • I don't know what it does as the above code is copied from somewhere and I am no expert in HTML canvas – Archit Gargi Mar 28 '22 at 10:03

5 Answers5

1

I don't think there is an easy fix for what you are asking. You would need to store each particle into a buffer array with details about its color, position and so on.

You would need to then loop through the buffer and update any changes.

Then redraw each particle from the buffer to a freshly cleared screen.

It's all very doable but very heavy on the browser depending whether its a phone or a desktop tablet etc.

The easiest fix is the background being solid which is what you had.

Else you could be clever and use looping video in either AV1 or VP9 codecs as they have wide support for transparency.

If these options do not suite then coding it will be the only way I can see it working but anything more than a 1000 particles may cause fat hang ups and lag and not the buttery smooth FPS you would expect.

If anyone else knows a better solution for this problem then I would love to know :D

  • Thanks for giving the solution in theory at least. If anyone have a solution to this, pls answer and I am perfectly fine if it works only on certain settings in browser or is a temporary fix or is not responsive because I need to show it only once to my mother since its a birthday card. – Archit Gargi Mar 28 '22 at 12:40
1

One solution is to use many layers, which will be responsible of the overall fading.

So at each frame, you create a new layer, and you decrease the opacity of all the previous ones. When that opacity is below a certain threshold, you remove it from the list.
Then you change your draw() methods to register the drawing commands in these layers instead of making the context draw directly. The layer will be responsible of setting the correct alpha (based on its own opacity and the one of the item to be drawn), and it will draw all the items from its frame.

Basically these layers will be still of the current frame, but instead of storing the pixels data, we only store the drawing commands. This is quite easy in your situation since you have a rather limited number of drawing settings (only color, alpha, x, and y), but someone else may need a more complex API.

Anyway, here is a messy rewrite as a proof-of-concept:

class Layer {
  constructor(ctx) {
    this.ctx = ctx;
    this.commands = [];
    this.alpha = 1;
  }
  update(delta) {
    this.alpha *= 1 - (0.1 * delta);
    return this.alpha < 0.1;
  }
  draw() {
    this.commands.forEach(([color, alpha, x, y, width, height]) => {
      this.ctx.fillStyle = color;
      this.ctx.globalAlpha = this.alpha * alpha;
      this.ctx.fillRect(x, y, width, height);
    });
  }
}
class Particle {
  constructor(x, y, col) {
    this.x = x;
    this.y = y;
    this.col = col;
    this.vel = randomVec(2);
    this.lifetime = 0;
  }

  update(delta) {
    delta = 1;
    this.x += this.vel.x;
    this.y += this.vel.y;
    this.vel.y += 0.02 * delta;
    this.vel.x *= 1 - (delta * 0.01);
    this.vel.y *= 1 - (delta * 0.01);
    this.lifetime += delta;
    return this.lifetime > 80;
  }

  draw() {
    const color = this.col;
    const alpha = Math.max(1 - this.lifetime / 80, 0);
    const x = this.x;
    const y = this.y;
    const rad = 2;
    currentLayer.commands.push([color, alpha, x, y, rad, rad]);
  }
}

class Firework {
  constructor(x) {
    this.x = x;
    this.y = height;
    this.isBlown = false;
    this.col = randomCol();
  }

  update(delta) {
    this.y -= 3 * delta;
    if (this.y < 350 - Math.sqrt(Math.random() * 500) * 40) {
      this.isBlown = true;
      for (let i = 0; i < 60; i++) {
        particles.push(new Particle(this.x, this.y, this.col))
      }
    }
    return this.isBlown;
  }

  draw() {
    const color = this.col;
    const alpha = 1;
    const x = this.x;
    const y = this.y;
    const rad = 2;
    currentLayer.commands.push([color, alpha, x, y, rad, rad]);
  }
}

const canvas = document.querySelector("canvas");
let width, height, currentLayer;
const layers = [];
const fireworks = [];
const particles = [];
setSize(canvas);
const ctx = canvas.getContext("2d");
fireworks.push(new Firework(Math.random() * (width - 200) + 100));

window.addEventListener("resize", windowResized);
document.addEventListener("click", onClick);

let lastTime = document.timeline?.currentTime || performance.now();
requestAnimationFrame(loop);

function loop(time) {
  // values were defined with a 60FPS base
  // we now use rAF and thus need a delta time
  // to honor the same expected speed on all devices
  const delta = (time - lastTime) / (1000 / 60);
  lastTime = time;
  ctx.globalAlpha = 1;
  ctx.clearRect(0, 0, width, height);
  currentLayer = new Layer(ctx);
  layers.push(currentLayer);

  const ended = [];
  fireworks.forEach((firework, index) => {
    const done = firework.update(delta);
    if (done) {
      ended.push(index);
    }
    firework.draw();
  });
  // remove all ended, for last to first
  ended.reverse().forEach((index) => {
    fireworks.splice(index, 1);
  });
  ended.length = 0;

  particles.forEach((particle, index) => {
    const done = particle.update(delta);
    particle.draw();
    if (done) {
      ended.push(index);
    }
  });

  ended.reverse().forEach((index) => {
    particles.splice(index, 1);
  });
  ended.length = 0;

  layers.forEach((layer, index) => {
    const done = layer.update(delta);
    if (done) {
      ended.push(index);
    }
    layer.draw();
  });

  ended.reverse().forEach((index) => {
    layers.splice(index, 1);
  });

  if (Math.random() < 1 / 60) {

    fireworks.push(new Firework(Math.random() * (width - 200) + 100));
  }
  requestAnimationFrame(loop);
}

function randomCol() {
  var letter = '0123456789ABCDEF';
  var nums = [];

  for (var i = 0; i < 3; i++) {
    nums[i] = Math.floor(Math.random() * 256);
  }

  let brightest = 0;
  for (var i = 0; i < 3; i++) {
    if (brightest < nums[i]) brightest = nums[i];
  }

  brightest /= 255;
  for (var i = 0; i < 3; i++) {
    nums[i] /= brightest;
  }

  let color = "#";
  for (var i = 0; i < 3; i++) {
    color += letter[Math.floor(nums[i] / 16)];
    color += letter[Math.floor(nums[i] % 16)];
  }
  return color;
}

function randomVec(max) {
  let dir = Math.random() * Math.PI * 2;
  let spd = Math.random() * max;
  return {
    x: Math.cos(dir) * spd,
    y: Math.sin(dir) * spd
  };
}

function setSize(canv) {
  canv.style.width = (innerWidth) + "px";
  canv.style.height = (innerHeight) + "px";
  width = innerWidth;
  height = innerHeight;

  canv.width = innerWidth * window.devicePixelRatio;
  canv.height = innerHeight * window.devicePixelRatio;
  canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}

function onClick(e) {
  fireworks.push(new Firework(e.clientX));
}

function windowResized() {
  setSize(canvas);
}
body{margin:0;/* checkered effect from https://stackoverflow.com/a/51054396/3702797 */
  --tint:rgba(255,255,255,0.9);background-image:linear-gradient(to right,var(--tint),var(--tint)),linear-gradient(to right,black 50%,white 50%),linear-gradient(to bottom,black 50%,white 50%);background-blend-mode:normal,difference,normal;background-size:2em 2em;
}
canvas{display: block}
<canvas></canvas>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
0

change fill style to:

ctx.fillStyle = "rgba(0,0,0,0)";
ctx.fillRect(0, 0, width, height);
TwistedOwl
  • 1,195
  • 1
  • 17
  • 30
  • Well, it does do its job but it doesn't erase the fireworks once they have blown up. Pls try it on the actual code before answering :( – Archit Gargi Mar 28 '22 at 10:30
  • I think the problem is bigger. That gives the transparent background but you need to completely clear the buffer between each frame. Ie use ctx.clear() between frames. problem is you end up deleting all the previous particles so you need to redraw the particles you need for that frame and only that frame. It works well with a solid background because you can fill a white box then draw on top but the particles are still left behind –  Mar 28 '22 at 10:59
  • yes exactly.... – Archit Gargi Mar 28 '22 at 11:01
0

globalCompositeOperation

To clear only pixels that have an alpha > 0 use the globalCompositeOperation property and set it to "destination-out".

This will let you partially delete the pixels (leaving the trails) without effecting the background.

You will need to restore the composite operation to "source-over".

The change to the function loop as follows

function loop(){
        ctx.globalAlpha = 0.1;
        ctx.globalCompositeOperation = "destination-out";   // Add line
        ctx.fillStyle = "#000000";
        ctx.fillRect(0, 0, width, height);
        ctx.globalCompositeOperation = "source-over";       // Add line 
        ctx.globalAlpha = 1;

        //... rest of code

Example

Snippet is a copy of your code with the above changes.

Also add slight change to stop it crashing on SO snippet due to improper setup call order.

"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];

function setup() {
    canvas = document.getElementById("canvas");
    setSize(canvas);
    ctx = canvas.getContext("2d");
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);
    fireworks.push(new Firework(Math.random()*(width-200)+100));
    window.addEventListener("resize",windowResized);
    document.addEventListener("click",onClick);
    setInterval(loop, 1/60);
}

setTimeout(setup,1);
var frameCount = 0;
function loop(){
    if (frameCount++ % 2) {
        ctx.globalAlpha = 0.2;
        ctx.globalCompositeOperation = "destination-out";
        ctx.fillStyle = "#000000";
        ctx.fillRect(0, 0, width, height);
        ctx.globalCompositeOperation = "source-over";
        ctx.globalAlpha = 1;
    }

    for(let i=0; i<fireworks.length; i++){
        let done = fireworks[i].update();
        fireworks[i].draw();
        if(done) fireworks.splice(i, 1);
    }

    for(let i=0; i<particles.length; i++){
        particles[i].update();
        particles[i].draw();
        if(particles[i].lifetime>80) particles.splice(i,1);
    }

    if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}


class Particle{
    constructor(x, y, col){
        this.x = x;
        this.y = y;
        this.col = col;
        this.vel = randomVec(2);
        this.lifetime = 0;
    }

    update(){
        this.x += this.vel.x;
        this.y += this.vel.y;
        this.vel.y += 0.02;
        this.vel.x *= 0.99;
        this.vel.y *= 0.99;
        this.lifetime++;
    }

    draw(){
        ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
        ctx.fillStyle = this.col;
        ctx.fillRect(this.x, this.y, 2, 2);
    }
}

class Firework{
    constructor(x){
        this.x = x;
        this.y = height;
        this.isBlown = false;
        this.col = randomCol();
    }

    update(){
        this.y -= 3;
        if(this.y < 350-Math.sqrt(Math.random()*500)*40){
            this.isBlown = true;
            for(let i=0; i<60; i++){
                particles.push(new Particle(this.x, this.y, this.col))
            }
        }
        return this.isBlown;
        
    }

    draw(){
        ctx.globalAlpha = 1;
        ctx.fillStyle = this.col;
        ctx.fillRect(this.x, this.y, 2, 2);
    }
}

function randomCol(){
    var letter = '0123456789ABCDEF';
    var nums = [];

    for(var i=0; i<3; i++){
        nums[i] = Math.floor(Math.random()*256);
    }

    let brightest = 0;
    for(var i=0; i<3; i++){
        if(brightest<nums[i]) brightest = nums[i];
    }

    brightest /=255;
    for(var i=0; i<3; i++){
        nums[i] /= brightest;
    }

    let color = "#";
    for(var i=0; i<3; i++){
        color += letter[Math.floor(nums[i]/16)];
        color += letter[Math.floor(nums[i]%16)];
    }
    return color;
}

function randomVec(max){
    let dir = Math.random()*Math.PI*2;
    let spd = Math.random()*max;
    return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}

function setSize(canv){
    canv.style.width = (innerWidth) + "px";
    canv.style.height = (innerHeight) + "px";
    width = innerWidth;
    height = innerHeight;

    canv.width = innerWidth*window.devicePixelRatio;
    canv.height = innerHeight*window.devicePixelRatio;
    canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}

function onClick(e){
    fireworks.push(new Firework(e.clientX));
}

function windowResized(){
    setSize(canvas);
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);
}
body {
 background: #333;
}
canvas {
   position: absolute;
   top: 0px;
   left: 0px;
}
p { color: white; }
h1 { color: white; }
code { background: #DEF; }
<canvas id="canvas"></canvas>
    <h1>Trails</h1>
    <p>Text under the canvas with trails using</p>
    <code>ctx.globalCompositeOperation = "destination-out";</code>

Update for above snipet

Removed residual pixels from example by doubling the clear alpha value and halving the clear rate.

Blindman67
  • 51,134
  • 11
  • 73
  • 136
-1

As I understand you wanted to have a white background for your canvas? Check this out!

"use strict";
let canvas, width, height, ctx;
let fireworks = [];
let particles = [];

function setup() {
    canvas = document.getElementById("canvas");
    setSize(canvas);
    ctx = canvas.getContext("2d");
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);
    fireworks.push(new Firework(Math.random()*(width-200)+100));
    window.addEventListener("resize",windowResized);
    document.addEventListener("click",onClick);
}

setTimeout(setup,1);

function loop(){
    ctx.globalAlpha = 0.1;
    ctx.fillStyle = "#ffffff";
    ctx.fillRect(0, 0, width, height);
    ctx.globalAlpha = 1;

    for(let i=0; i<fireworks.length; i++){
        let done = fireworks[i].update();
        fireworks[i].draw();
        if(done) fireworks.splice(i, 1);
    }

    for(let i=0; i<particles.length; i++){
        particles[i].update();
        particles[i].draw();
        if(particles[i].lifetime>80) particles.splice(i,1);
    }

    if(Math.random()<1/60) fireworks.push(new Firework(Math.random()*(width-200)+100));
}
setInterval(loop, 1/60);
//setInterval(loop, 100/60);
class Particle{
    constructor(x, y, col){
        this.x = x;
        this.y = y;
        this.col = col;
        this.vel = randomVec(2);
        this.lifetime = 0;
    }

    update(){
        this.x += this.vel.x;
        this.y += this.vel.y;
        this.vel.y += 0.02;
        this.vel.x *= 0.99;
        this.vel.y *= 0.99;
        this.lifetime++;
    }

    draw(){
        ctx.globalAlpha = Math.max(1-this.lifetime/80, 0);
        ctx.fillStyle = this.col;
        ctx.fillRect(this.x, this.y, 2, 2);
    }
}

class Firework{
    constructor(x){
        this.x = x;
        this.y = height;
        this.isBlown = false;
        this.col = randomCol();
    }

    update(){
        this.y -= 3;
        if(this.y < 350-Math.sqrt(Math.random()*500)*40){
            this.isBlown = true;
            for(let i=0; i<60; i++){
                particles.push(new Particle(this.x, this.y, this.col))
            }
        }
        return this.isBlown;
        
    }

    draw(){
        ctx.globalAlpha = 1;
        ctx.fillStyle = this.col;
        ctx.fillRect(this.x, this.y, 2, 2);
    }
}

function randomCol(){
    var letter = '0123456789ABCDEF';
    var nums = [];

    for(var i=0; i<3; i++){
        nums[i] = Math.floor(Math.random()*256);
    }

    let brightest = 0;
    for(var i=0; i<3; i++){
        if(brightest<nums[i]) brightest = nums[i];
    }

    brightest /=255;
    for(var i=0; i<3; i++){
        nums[i] /= brightest;
    }

    let color = "#";
    for(var i=0; i<3; i++){
        color += letter[Math.floor(nums[i]/16)];
        color += letter[Math.floor(nums[i]%16)];
    }
    return color;
}

function randomVec(max){
    let dir = Math.random()*Math.PI*2;
    let spd = Math.random()*max;
    return{x: Math.cos(dir)*spd, y: Math.sin(dir)*spd};
}

function setSize(canv){
    canv.style.width = (innerWidth) + "px";
    canv.style.height = (innerHeight) + "px";
    width = innerWidth;
    height = innerHeight;

    canv.width = innerWidth*window.devicePixelRatio;
    canv.height = innerHeight*window.devicePixelRatio;
    canvas.getContext("2d").scale(window.devicePixelRatio, window.devicePixelRatio);
}

function onClick(e){
    fireworks.push(new Firework(e.clientX));
}

function windowResized(){
    setSize(canvas);
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, width, height);
}
    <canvas id="canvas">
          <h1>Hey Try this out!</h1>
</canvas>
Milan Sachani
  • 335
  • 3
  • 8
  • No that's not what I want, I want to make the canvas transparent so that the elements behind it are shown. – Archit Gargi Mar 28 '22 at 10:23
  • which means you want to have text or any element in the middle of the canvas? so Text is visible but the firecracker is also visible but behind the text or any other element? – Milan Sachani Mar 28 '22 at 10:25
  • The firecrackers should appear not behind but in front of the element. My exact webpage where I use this has a background image and some text and I want to show that too and the firecrackers should appear on the top of the webpage. – Archit Gargi Mar 28 '22 at 10:27
  • @ArchitGargi these sort of details are useful _in the actual question_. We're not mindreaders! – Jamiec Mar 28 '22 at 10:42
  • Yes, Now I understood because the question which you have asked was about why my bg is showing black. that's why cool I will try to solve this :) – Milan Sachani Mar 28 '22 at 10:43