Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HTML5 canvas particle explosion

I'm trying to get this particle explosion working. It's working but it looks like some frames does not get rendered. If I click many times to call several explosions it starts to uhm.. "lag/stutter". Is there something I have forgotten to do? It may look like the browser hangs when I click many times. Is it too much to have 2 for loops inside each other?

Attached my code so you can see. Just try to click many times, and you will see the problem visually.

// Request animation frame
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
// Canvas
var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
// Set full-screen
c.width = window.innerWidth;
c.height = window.innerHeight;
// Options
var background = '#333'; // Background color
var particlesPerExplosion = 20;
var particlesMinSpeed = 3;
var particlesMaxSpeed = 6;
var particlesMinSize = 1;
var particlesMaxSize = 3;
var explosions = [];
var fps = 60;
var now, delta;
var then = Date.now();
var interval = 1000 / fps;
// Optimization for mobile devices
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
    fps = 29;
}
// Draw
function draw() {
    // Loop
    requestAnimationFrame(draw);
    // Set NOW and DELTA
    now = Date.now();
    delta = now - then;
    // New frame
    if (delta > interval) {
        // Update THEN
        then = now - (delta % interval);
        // Our animation
        drawBackground();
        drawExplosion();
    }
}
// Draw explosion(s)
function drawExplosion() {
    if (explosions.length == 0) {
        return;
    }
    for (var i = 0; i < explosions.length; i++) {
        var explosion = explosions[i];
        var particles = explosion.particles;
        if (particles.length == 0) {
            explosions.splice(i, 1);
            return;
        }
        for (var ii = 0; ii < particles.length; ii++) {
            var particle = particles[ii];
            // Check particle size
            // If 0, remove
            if (particle.size < 0) {
                particles.splice(ii, 1);
                return;
            }
            ctx.beginPath();
            ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
            ctx.closePath();
            ctx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
            ctx.fill();
            // Update
            particle.x += particle.xv;
            particle.y += particle.yv;
            particle.size -= .1;
        }
    }
}
// Draw the background
function drawBackground() {
    ctx.fillStyle = background;
    ctx.fillRect(0, 0, c.width, c.height);
}
// Clicked
function clicked(e) {
    var xPos, yPos;
    if (e.offsetX) {
        xPos = e.offsetX;
        yPos = e.offsetY;
    } else if (e.layerX) {
        xPos = e.layerX;
        yPos = e.layerY;
    }
    explosions.push(new explosion(xPos, yPos));
}
// Explosion
function explosion(x, y) {
    this.particles = [];
    for (var i = 0; i < particlesPerExplosion; i++) {
        this.particles.push(new particle(x, y));
    }
}
// Particle
function particle(x, y) {
    this.x = x;
    this.y = y;
    this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
    this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
    this.size = randInt(particlesMinSize, particlesMaxSize, true);
    this.r = randInt(113, 222);
    this.g = '00';
    this.b = randInt(105, 255);
}
// Returns an random integer, positive or negative
// between the given value
function randInt(min, max, positive) {
    if (positive == false) {
        var num = Math.floor(Math.random() * max) - min;
        num *= Math.floor(Math.random() * 2) == 1 ? 1 : -1;
    } else {
        var num = Math.floor(Math.random() * max) + min;
    }
    return num;
}
// On-click
$('canvas').on('click', function(e) {
    clicked(e);
});
draw();
<!DOCTYPE html>
<html>

<head>
    <style>
        * {
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

</html>
like image 871
Kaizokupuffball Avatar asked Apr 19 '17 14:04

Kaizokupuffball


2 Answers

You are returning from iterating over the particles if one is too small. This causes the other particles of that explosion to render only in the next frame.

I have a working version:

// Request animation frame
const requestAnimationFrame = window.requestAnimationFrame ||
  window.mozRequestAnimationFrame ||
  window.webkitRequestAnimationFrame ||
  window.msRequestAnimationFrame;

// Canvas
const c   = document.getElementById('canvas');
const ctx = c.getContext('2d');

// Set full-screen
c.width  = window.innerWidth;
c.height = window.innerHeight;

// Options
const background            = '#333';                    // Background color
const particlesPerExplosion = 20;
const particlesMinSpeed     = 3;
const particlesMaxSpeed     = 6;
const particlesMinSize      = 1;
const particlesMaxSize      = 3;
const explosions            = [];

let fps        = 60;
const interval = 1000 / fps;

let now, delta;
let then = Date.now();

// Optimization for mobile devices
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
  fps = 29;
}

// Draw
function draw() {
  // Loop
  requestAnimationFrame(draw);

  // Set NOW and DELTA
  now   = Date.now();
  delta = now - then;

  // New frame
  if (delta > interval) {

    // Update THEN
    then = now - (delta % interval);

    // Our animation
    drawBackground();
    drawExplosion();

  }

}

// Draw explosion(s)
function drawExplosion() {

  if (explosions.length === 0) {
    return;
  }

  for (let i = 0; i < explosions.length; i++) {

    const explosion = explosions[i];
    const particles = explosion.particles;

    if (particles.length === 0) {
      explosions.splice(i, 1);
      return;
    }

    const particlesAfterRemoval = particles.slice();
    for (let ii = 0; ii < particles.length; ii++) {

      const particle = particles[ii];

      // Check particle size
      // If 0, remove
      if (particle.size <= 0) {
        particlesAfterRemoval.splice(ii, 1);
        continue;
      }

      ctx.beginPath();
      ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
      ctx.closePath();
      ctx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
      ctx.fill();

      // Update
      particle.x += particle.xv;
      particle.y += particle.yv;
      particle.size -= .1;
    }

    explosion.particles = particlesAfterRemoval;

  }

}

// Draw the background
function drawBackground() {
  ctx.fillStyle = background;
  ctx.fillRect(0, 0, c.width, c.height);
}

// Clicked
function clicked(e) {

  let xPos, yPos;

  if (e.offsetX) {
    xPos = e.offsetX;
    yPos = e.offsetY;
  } else if (e.layerX) {
    xPos = e.layerX;
    yPos = e.layerY;
  }

  explosions.push(
    new explosion(xPos, yPos)
  );

}

// Explosion
function explosion(x, y) {

  this.particles = [];

  for (let i = 0; i < particlesPerExplosion; i++) {
    this.particles.push(
      new particle(x, y)
    );
  }

}

// Particle
function particle(x, y) {
  this.x    = x;
  this.y    = y;
  this.xv   = randInt(particlesMinSpeed, particlesMaxSpeed, false);
  this.yv   = randInt(particlesMinSpeed, particlesMaxSpeed, false);
  this.size = randInt(particlesMinSize, particlesMaxSize, true);
  this.r    = randInt(113, 222);
  this.g    = '00';
  this.b    = randInt(105, 255);
}

// Returns an random integer, positive or negative
// between the given value
function randInt(min, max, positive) {

  let num;
  if (positive === false) {
    num = Math.floor(Math.random() * max) - min;
    num *= Math.floor(Math.random() * 2) === 1 ? 1 : -1;
  } else {
    num = Math.floor(Math.random() * max) + min;
  }

  return num;

}

// On-click
$('canvas').on('click', function (e) {
  clicked(e);
});

draw();
<!DOCTYPE html>

<html>

	<head>
		<style>* {margin:0;padding:0;overflow:hidden;}</style>
	</head>

    <body>
        <canvas id="canvas"></canvas>
    </body>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    </html>
like image 57
thedude Avatar answered Sep 23 '22 01:09

thedude


Loops, break and continue.

The problem was caused when you checked for empty particle arrays and when you found a particle to remove.

The bugs

The following two statements and blocks caused the problem

if (particles.length == 0) {
    explosions.splice(i, 1);
    return;
}

and

if (particles.size < 0) {
    explosions.splice(ii, 1);
    return;
}

The returns stopped the rendering of particles, so you would sometimes return before drawing a single particle was rendered just because the first explosion was empty or first particle was too small.

Continue and break

You can use the continue token in javascript to skip the rest of a for, while, do loop

for(i = 0; i < 100; i++){
    if(test(i)){
        // need to skip this iteration
        continue;
    }
    // more code 
    // more code 
    // continue skips all the code upto the closing }

} << continues to here and if i < 100 the loop continues on.

Or you can completely break out of the loop with break

for(i = 0; i < 100; i++){
    if(test(i)){
        // need to exit the for loop
        break;
    }
    // more code 
    // more code 
    // break skips all the code to the first line after the closing }

} 
<< breaks to here and if i remains the value it was when break was encountered

The fix

if (particles.length == 0) {
    explosions.splice(i, 1);
    continue;
}

and

if (particles.size < 0) {
    explosions.splice(ii, 1);
    continue;
}

Your example with the fix

Your code with the fix. Befor I found it I started changing stuff.

Minor stuff. requestAnimationFrame passes a time in milliseconds so to an accuracy of micro seconds.

You were setting then incorrectly and would have been losing frames. I changed the timing to use the argument time and then is just set to the time when a frame is drawn.

There are some other issues, nothing major and more of a coding style thing. You should capitalise objects created with new

function Particle(...

not

function particle(...

and your random is a overly complex

function randInt(min, max = min - (min = 0)) {
    return Math.floor(Math.random() * (max - min) + min);
}

or

function randInt(min,max){
     max = max === undefined ? min - (min = 0) : max;
     return Math.floor(Math.random() * (max - min) + min);
}

randInt(100); // int 0 - 100
randInt(10,20); // int 10-20
randInt(-100); // int -100 to 0
randInt(-10,20); // int -10 to 20

this.xv = randInt(-particlesMinSpeed, particlesMaxSpeed);
this.yv = randInt(-particlesMinSpeed, particlesMaxSpeed);
this.size = randInt(particlesMinSize, particlesMaxSize);

And if you are using the same name in variables a good sign to create an object

var particlesPerExplosion = 20;
var particlesMinSpeed = 3;
var particlesMaxSpeed = 6;
var particlesMinSize = 1;
var particlesMaxSize = 3;

Could be

const settings = {
    particles : {
         speed : {min : 3, max : 6 },
         size : {min : 1 : max : 3 },
         explosionCount : 20,
    },
    background : "#000",
 }

Anyways your code.

var c = canvas;
var ctx = c.getContext('2d');
// Set full-screen
c.width = innerWidth;
c.height = innerHeight;
// Options
var background = '#333'; // Background color
var particlesPerExplosion = 20;
var particlesMinSpeed = 3;
var particlesMaxSpeed = 6;
var particlesMinSize = 1;
var particlesMaxSize = 3;
var explosions = [];
var fps = 60;
var now, delta;
var then = 0;  // Zero start time 
var interval = 1000 / fps;
// Optimization for mobile devices
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
    fps = 29;
}
// Draw
// as time is passed you need to start with requestAnimationFrame
requestAnimationFrame(draw);
function draw(time) {  //requestAnimationFrame frame passes the time
    requestAnimationFrame(draw);
    delta = time - then;
    if (delta > interval) {
        then = time
        drawBackground();
        drawExplosion();
    }
}
// Draw explosion(s)
function drawExplosion() {
    if (explosions.length == 0) {
        return;
    }
    for (var i = 0; i < explosions.length; i++) {
        var explosion = explosions[i];
        var particles = explosion.particles;
        if (particles.length == 0) {
            explosions.splice(i, 1);
            //return;
            continue;
        }
        for (var ii = 0; ii < particles.length; ii++) {
            var particle = particles[ii];
            // Check particle size
            // If 0, remove
            if (particle.size < 0) {
                particles.splice(ii, 1);
               // return;
               continue;
            }
            ctx.beginPath();
            ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
            ctx.closePath();
            ctx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
            ctx.fill();
            // Update
            particle.x += particle.xv;
            particle.y += particle.yv;
            particle.size -= .1;
        }
    }
}
// Draw the background
function drawBackground() {
    ctx.fillStyle = background;
    ctx.fillRect(0, 0, c.width, c.height);
}
// Clicked
function clicked(e) {
    var xPos, yPos;
    if (e.offsetX) {
        xPos = e.offsetX;
        yPos = e.offsetY;
    } else if (e.layerX) {
        xPos = e.layerX;
        yPos = e.layerY;
    }
    explosions.push(new explosion(xPos, yPos));
}
// Explosion
function explosion(x, y) {
    this.particles = [];
    for (var i = 0; i < particlesPerExplosion; i++) {
        this.particles.push(new particle(x, y));
    }
}
// Particle
function particle(x, y) {
    this.x = x;
    this.y = y;
    this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
    this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
    this.size = randInt(particlesMinSize, particlesMaxSize, true);
    this.r = randInt(113, 222);
    this.g = '00';
    this.b = randInt(105, 255);
}
// Returns an random integer, positive or negative
// between the given value
function randInt(min, max, positive) {
    if (positive == false) {
        var num = Math.floor(Math.random() * max) - min;
        num *= Math.floor(Math.random() * 2) == 1 ? 1 : -1;
    } else {
        var num = Math.floor(Math.random() * max) + min;
    }
    return num;
}
// On-click
$('canvas').on('click', function(e) {
    clicked(e);
});
<!DOCTYPE html>
<html>

<head>
    <style>
        * {
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
    </style>
</head>

<body>
    <canvas id="canvas"></canvas>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

</html>
like image 24
Blindman67 Avatar answered Sep 23 '22 01:09

Blindman67