2

I tried to create a canvas effect with fireworks, but the more you click, the faster it gets and it seems to accumulate on itself. When I listed the speed it was similar and did not correspond to what was happening there. I also tried to cancel the draw if it got out of the canvas but it didn´t help. Here is link https://dybcmwd8icxxdxiym4xkaw-on.drv.tw/canvasTest.html

var fireAr = [];
var expAr = [];

function Firework(x, y, maxY, maxX, cn, s, w, en) {
    this.x = x;
    this.y = y;
    this.maxY = maxY;
    this.maxX = maxX;
    this.cn = cn;
    this.s = s;
    this.w = w;
    this.en = en;
    this.i = 0;

    this.explosion = function() {
        for (; this.i < this.en; this.i++) {
            var ey = this.maxY;
            var ex = this.maxX;
            var ecn = Math.floor(Math.random() * color.length);
            var esX = (Math.random() - 0.5) * 3;
            var esY = (Math.random() - 0.5) * 3;
            var ew = Math.random() * 10;
            var t = true;
            expAr.push(new Exp(ew, esX, esY, ex, ey, ecn, t));

        }
        for (var e = 0; e < expAr.length; e++) {
            expAr[e].draw();
        }
    }
    this.draw = function() {
        if (this.y < this.maxY) {
            this.explosion();

        } else {
            this.track();
            this.y -= this.s;
        }
    }
}

function Exp(ew, esX, esY, ex, ey, ecn, t) {
    this.ew = ew;
    this.esX = esX;
    this.esY = esY;
    this.ex = ex;
    this.ey = ey;
    this.ecn = ecn;
    this.t = t;
    this.draw = function() {
        if (this.t == true) {
            c.beginPath();
            c.shadowBlur = 20;
            c.shadowColor = color[this.ecn];
            c.rect(this.ex, this.ey, this.ew, this.ew);
            c.fillStyle = color[this.ecn];
            c.fill();
            c.closePath();
            this.ex += this.esX;
            this.ey += this.esY;
        }
    }
}

window.addEventListener('click', function(event) {
    var x = event.clientX;
    var y = canvas.height;
    mouse.clickX = event.clientX;
    mouse.clickY = event.clientY;
    var maxY = event.clientY;
    var maxX = event.clientX;
    var cn = Math.floor(Math.random() * color.length);
    var s = Math.random() * 5 + 5;
    var w = Math.random() * 20 + 2;
    var en = Math.random() * 50 + 5;
    fireAr.push(new Firework(x, y, maxY, maxX, cn, s, w, en));
});

function ani() {
    requestAnimationFrame(ani);
    c.clearRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < fireAr.length; i++) {
        fireAr[i].draw();

    }
}
ani();

I deleted some unnecessary parts in my opinion but if I'm wrong and I missed something I'll try to fix it

ggorlen
  • 44,755
  • 7
  • 76
  • 106
zxcv6
  • 23
  • 3

1 Answers1

4

Here are a few simple ways you can improve performance:

  • Commenting out shadowBlur gives a noticeable boost. If you need shadows, see this answer which illustrates pre-rendering.
  • Try using fillRect and ctx.rotate() instead of drawing a path. Saving/rotating/restoring the canvas might be prohibitive, so you could use non-rotated rectangles.
  • Consider using a smaller canvas which is quicker to repaint than one that may fill the entire window.

Another issue is more subtle: Fireworks and Exps are being created (making objects is expensive!) and pushed onto arrays. But these arrays are never trimmed and objects are never reused after they've left the visible canvas. Eventually, the rendering loop gets bogged down by all of the computation for updating and rendering every object in the fireAr and expAr arrays.

A naive solution is to check for objects exiting the canvas and splice them from the expAr. Here's pseudocode:

for (let i = expAr.length - 1; i >= 0; i--) {
  if (!inBounds(expAr[i], canvas)) {
    expAr.splice(i, 1);
  }
}

Iterate backwards since this mutates the array's length. inBounds is a function that checks an Exp object's x and y properties along with its size and the canvas width and height to determine if it has passed an edge. More pseudocode:

function inBounds(obj, canvas) {
  return obj.x >= 0 && obj.x <= canvas.width &&
         obj.y >= 0 && obj.y <= canvas.height;
}

This check isn't exactly correct since the rectangles are rotated. You could check each corner of the rectangle with a pointInRect function to ensure that at least one is inside the canvas.

Fireworks can be spliced out when they "explode".

splice is an expensive function that walks up to the entire array to shift items forward to fill in the vacated element. Splicing multiple items in a loop gives quadratic performance. This can be made linear by putting surviving fireworks in a new list and replacing the previous generation on each frame. Dead firework objects can be saved in a pool for reuse.

Beyond that, I strongly recommend using clear variable names.

this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;

These names have little or no meaning to an outside reader and are unlikely to mean much to you if you take a couple months away from the code. Use full words like "size", "width", etc.

Another side point is that it's a good idea to debounce your window resize listener.

Here's a quick proof of concept that illustrates the impact of shadowBlur and pruning dead elements.

const rnd = n => ~~(Math.random() * n);
const mouse = {pressed: false, x: 0, y: 0};
let fireworks = [];
let shouldSplice = false;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

document.body.appendChild(canvas);
document.body.style.margin = 0;
canvas.style.background = "#111";
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
ctx.shadowBlur = 0;

const fireworksAmt = document.querySelector("#fireworks-amt");
document.querySelector("input[type=range]").addEventListener("change", e => {
  ctx.shadowBlur = e.target.value;
  document.querySelector("#shadow-amt").textContent = ctx.shadowBlur;
});
document.querySelector("input[type=checkbox]").addEventListener("change", e => {
  shouldSplice = !shouldSplice;
});

const createFireworks = (x, y) => {
  const color = `hsl(${rnd(360)}, 100%, 60%)`;
  return Array(rnd(20) + 1).fill().map(_ => ({
    x: x,
    y: y,
    vx: Math.random() * 6 - 3,
    vy: Math.random() * 6 - 3,
    size: rnd(4) + 2,
    color: color
  }));
}
  
(function render() {
  if (mouse.pressed) {
    fireworks.push(...createFireworks(mouse.x, mouse.y));
  }

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  for (const e of fireworks) {
    e.x += e.vx;
    e.y += e.vy;
    e.vy += 0.03;
        
    ctx.beginPath();
    ctx.fillStyle = ctx.shadowColor = e.color;
    ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2);
    ctx.fill();
 
    if (shouldSplice) {
      e.size -= 0.03;
      
      if (e.size < 1) {
        e.dead = true;
      }
    }
  }
  
  fireworks = fireworks.filter(e => !e.dead);
  fireworksAmt.textContent = "fireworks: " + fireworks.length;
  requestAnimationFrame(render);
})();

let debounce;
addEventListener("resize", e => {
  clearTimeout(debounce);
  debounce = setTimeout(() => {
    canvas.width = document.body.scrollWidth;
    canvas.height = document.body.clientHeight;
  }, 100);
});

canvas.addEventListener("mousedown", e => {
  mouse.pressed = true;
});
canvas.addEventListener("mouseup", e => {
  mouse.pressed = false;
});
canvas.addEventListener("mousemove", e => {
  mouse.x = e.offsetX;
  mouse.y = e.offsetY;
});
* {
  font-family: monospace;
  user-select: none;
}
div > span, body > div {padding: 0.5em;}
canvas {display: block;}
<div>
  <div id="fireworks-amt">fireworks: 0</div>
  <div>
    <label>splice? </label>
    <input type="checkbox">
  </div>
  <div>
    <label>shadowBlur (<span id="shadow-amt">0</span>): </label>
    <input type="range" value=0>
  </div>
</div>
ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • That would speed up the code, OP is complaining about an acceleration! Lol. (BTW I can't reproduce the acceleration) – Sheraff Jan 03 '20 at 23:16
  • Hmm, good point, although I just took that to mean that the jitteriness could be misinterpreted as acceleration I think. We'll have to wait for clarification from OP. – ggorlen Jan 03 '20 at 23:17
  • Thank you very much for the answer I'm not so good with javascript and it helped me a lot. Sorry about the variables I just jumped in and forgot to fix them. I'm just not sure if I understood this part well `et debounce; addEventListener("resize", e => { clearTimeout(debounce); debounce = setTimeout(() => { canvas.width = innerWidth; canvas.height = innerHeight; }, 100); }); ` – zxcv6 Jan 07 '20 at 19:09
  • Sure, no problem. That's somewhat irrelevant to the rest of the code but you can check the debounce link I shared above or [look at this thread](https://stackoverflow.com/questions/24004791/can-someone-explain-the-debounce-function-in-javascript). The point is that you don't want a series of window resizes (when the user drags the resize bar, for example) to clog up the app, just wait a couple hundred ms after they stop dragging to take action. – ggorlen Jan 07 '20 at 19:16
  • Okay, so if I resize, it will fire every 100 milliseconds until I resize – zxcv6 Jan 07 '20 at 19:25
  • And there is some library used in the code or it is pure javascript? Last question. – zxcv6 Jan 07 '20 at 19:28
  • 1
    No, that's throttling, which means "fire no more than 1 callback in a given `n` ms window". Debouncing means "do nothing until `n` ms after they stop triggering the event". See [this](https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function). The code above is vanilla JS but I'm using ES6 syntax. – ggorlen Jan 07 '20 at 19:28