Canvas particles, collisions and performance

I am creating a web application that has an interactive background with particles bouncing around. At all times, the screen has about 200 circular particles and not more than 800 particles. Some of the collisions and effects that are performed on particles are the following prototypes. I wonder if I can improve productivity by using web workers to perform these calculations?

/** * Particles */ Jarvis.prototype.genForegroundParticles = function(options, count){ count = count || this.logoParticlesNum; for (var i = 0; i < count; i++) { this.logoParticles.push(new Particle()); } } Jarvis.prototype.genBackgroundParticles = function(options, count){ count = count || this.backgroundParticlesNum; for (var i = 0; i < count; i++) { this.backgroundParticles.push(new Particle(options)); } } Jarvis.prototype.motion = { linear : function(particle, pIndex, particles){ particle.x += particle.vx particle.y += particle.vy }, normalizeVelocity : function(particle, pIndex, particles){ if (particle.vx - particle.vxInitial > 1) { particle.vx -= 0.05; } else if (particle.vx - particle.vxInitial < -1) { particle.vx += 0.05; } if (particle.vy - particle.vyInitial > 1) { particle.vy -= 0.05; } else if (particle.vx - particle.vxInitial < -1) { particle.vy += 0.05; } }, explode : function(particle, pIndex, particles) { if (particle.isBottomOut()) { particles.splice(pIndex, 1); } else { particle.x += particle.vx; particle.y += particle.vy; particle.vy += 0.1; } if (particles.length === 0){ particles.motion.removeMotion("explode"); this.allowMenu = true; } } } Jarvis.prototype.collision = { boundingBox: function(particle, pIndex, particles){ if (particle.y > (this.HEIGHT - particle.radius) || particle.y < particle.radius) { particle.vy *= -1; } if(particle.x > (this.WIDTH - particle.radius) || particle.x < particle.radius) { particle.vx *= -1; } }, boundingBoxGravity: function(particle, pIndex, particles){ // TODO: FIX GRAVITY TO WORK PROPERLY IN COMBINATION WITH FX AND MOTION if (particle.y > (this.HEIGHT - particle.radius) || particle.y < particle.radius) { particle.vy *= -1; particle.vy += 5; } if(particle.x > (this.WIDTH - particle.radius) || particle.x < particle.radius) { particle.vx *= -1; particle.vx += 5; } }, infinity: function(particle, pIndex, particles){ if (particle.x > this.WIDTH){ particle.x = 0; } if (particle.x < 0){ particle.x = this.WIDTH; } if (particle.y > this.HEIGHT){ particle.y = 0; } if (particle.y < 0) { particle.y = this.HEIGHT; } } } Jarvis.prototype.fx = { link : function(particle, pIndex, particles){ for(var j = pIndex + 1; j < particles.length; j++) { var p1 = particle; var p2 = particles[j]; var particleDistance = getDistance(p1, p2); if (particleDistance <= this.particleMinLinkDistance) { this.backgroundCtx.beginPath(); this.backgroundCtx.strokeStyle = "rgba("+p1.red+", "+p1.green+", "+p1.blue+","+ (p1.opacity - particleDistance / this.particleMinLinkDistance) +")"; this.backgroundCtx.moveTo(p1.x, p1.y); this.backgroundCtx.lineTo(p2.x, p2.y); this.backgroundCtx.stroke(); this.backgroundCtx.closePath(); } } }, shake : function(particle, pIndex, particles){ if (particle.xInitial - particle.x >= this.shakeAreaThreshold){ particle.xOper = (randBtwn(this.shakeFactorMin, this.shakeFactorMax) * 2) % (this.WIDTH); } else if (particle.xInitial - particle.x <= -this.shakeAreaThreshold) { particle.xOper = (randBtwn(-this.shakeFactorMax, this.shakeFactorMin) * 2) % (this.WIDTH); } if (particle.yInitial - particle.y >= this.shakeAreaThreshold){ particle.yOper = (randBtwn(this.shakeFactorMin, this.shakeFactorMax) * 2) % (this.HEIGHT); } else if (particle.yInitial - particle.y <= -this.shakeAreaThreshold) { particle.yOper = (randBtwn(-this.shakeFactorMax, this.shakeFactorMin) * 2) % (this.HEIGHT); } particle.x += particle.xOper; particle.y += particle.yOper; }, radialWave : function(particle, pIndex, particles){ var distance = getDistance(particle, this.center); if (particle.radius >= (this.dim * 0.0085)) { particle.radiusOper = -0.02; } else if (particle.radius <= 1) { particle.radiusOper = 0.02; } particle.radius += particle.radiusOper * particle.radius; }, responsive : function(particle, pIndex, particles){ var newPosX = (this.logoParticles.logoOffsetX + this.logoParticles.particleRadius) + (this.logoParticles.particleDistance + this.logoParticles.particleRadius) * particle.arrPos.x; var newPosY = (this.logoParticles.logoOffsetY + this.logoParticles.particleRadius) + (this.logoParticles.particleDistance + this.logoParticles.particleRadius) * particle.arrPos.y; if (particle.xInitial !== newPosX || particle.yInitial !== newPosY){ particle.xInitial = newPosX; particle.yInitial = newPosY; particle.x = particle.xInitial; particle.y = particle.yInitial; } }, motionDetect : function(particle, pIndex, particles){ var isClose = false; var distance = null; for (var i = 0; i < this.touches.length; i++) { var t = this.touches[i]; var point = { x : t.clientX, y : t.clientY } var d = getDistance(point, particle); if (d <= this.blackhole) { isClose = true; if (d <= distance || distance === null) { distance = d; } } } if (isClose){ if (particle.radius < (this.dim * 0.0085)) { particle.radius += 0.25; } if (particle.green >= 0 && particle.blue >= 0) { particle.green -= 10; particle.blue -= 10; } } else { if (particle.radius > particle.initialRadius) { particle.radius -= 0.25; } if (particle.green <= 255 && particle.blue <= 255) { particle.green += 10; particle.blue += 10; } } }, reverseBlackhole : function(particle, pIndex, particles){ for (var i = 0; i < this.touches.length; i++) { var t = this.touches[i]; var point = { x : t.clientX, y : t.clientY } var distance = getDistance(point, particle); if (distance <= this.blackhole){ var diff = getPointsDifference(point, particle); particle.vx += -diff.x / distance; particle.vy += -diff.y / distance; } } } } 

Also, if someone is wondering, I have 3 layers of canvas and I will add a particle rendering function and a clear function for all canvas layers

  • A background that draws a full radial gradient and particles on the screen.

  • Canvas menu

  • Buttons for selecting the overlay of menu buttons (show which menu is active, etc.)


 Jarvis.prototype.backgroundDraw = function() { // particles var that = this; this.logoParticles.forEach(function(particle, i){ particle.draw(that.backgroundCtx); that.logoParticles.motion.forEach(function(motionType, motionIndex){ that.motion[motionType].call(that, particle, i, that.logoParticles, "foregroundParticles"); }); that.logoParticles.fx.forEach(function(fxType, fxIndex){ that.fx[fxType].call(that, particle, i, that.logoParticles, "foregroundParticles"); }); that.logoParticles.collision.forEach(function(collisionType, collisionIndex){ that.collision[collisionType].call(that, particle, i, that.logoParticles, "foregroundParticles"); }); }); this.backgroundParticles.forEach(function(particle, i){ particle.draw(that.backgroundCtx); that.backgroundParticles.motion.forEach(function(motionType, motionIndex){ that.motion[motionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles"); }); that.backgroundParticles.fx.forEach(function(fxType, fxIndex){ that.fx[fxType].call(that, particle, i, that.backgroundParticles, "backgroundParticles"); }); that.backgroundParticles.collision.forEach(function(collisionType, collisionIndex){ that.collision[collisionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles"); }); }); } Jarvis.prototype.clearCanvas = function() { switch(this.background.type){ case "radial_gradient": this.setBackgroundRadialGradient(this.background.color1, this.background.color2); break; case "plane_color": this.setBackgroundColor(this.background.red, this.background.green, this.background.blue, this.background.opacity); break; default: this.setBackgroundColor(142, 214, 255, 1); } this.foregroundCtx.clearRect(this.clearStartX, this.clearStartY, this.clearDistance, this.clearDistance); this.middlegroundCtx.clearRect(this.clearStartX, this.clearStartY, this.clearDistance, this.clearDistance); } Jarvis.prototype.mainLoop = function() { this.clearCanvas(); this.backgroundDraw(); this.drawMenu(); window.requestAnimFrame(this.mainLoop.bind(this)); } 

Any other optimization tips are welcome. I have read several articles, but I'm not sure how to optimize this code.

+6
source share
4 answers

If you want to speed up the code, here are a few micro optimizations:

  • for(var i = 0, l = bla.length; i < l; i++) { ... } instead of bla.forEach(...)
  • reduce the use of callbacks. Built-in simple material.
  • Comparison with distance is slow due to SQRT. radius <= distance slow, radius*radius <= distanceSquared fast.
  • distance calculation is performed by calculating the difference. Now you make 2 function calls, first to get the distance, then to get the difference. here's a little rewrite: no function calls, no unnecessary calculations.

reverseBlackhole : function(particle, pIndex, particles) { var blackholeSqr = this.blackhole * this.blackhole, touches = this.touches, fnSqrt = Math.sqrt, t, diffX, diffY, dstSqr; for (var i = 0, l = touches.length; i < l; i++) { t = touches[i]; diffX = particle.x - t.clientX; diffY = particle.y - t.clientY; distSqr = (diffX * diffX + diffY * diffY); // comparing distance without a SQRT needed if (dstSqr <= blackholeSqr){ var dist = Math.sqrt(dstSqr); particle.vx -= diffX / dist; particle.vy -= diffY / dist; } } }

To speed up drawing (or make it smaller while drawing):

  • Separate your calculations from the drawing
  • Request a redraw after updating your calculations

And for the whole animation:

  • this.backgroundParticles.forEach(..) : in the case of 200 particles, this will be
    • 200 particles times ( this.backgroundParticles.forEach( )
      • 200 particles ( that.backgroundParticles.motion.forEach )
      • 200 particles ( that.backgroundParticles.fx.forEach )
      • 200 particles ( that.backgroundParticles.collision.forEach )
  • same for this.foregroundparticles.forEach(..)
  • let's say we have 200 background and 100 front, that is (200 * 200 * 3) + (100 * 100 * 3) callbacks, which is 150,000 callbacks, a tick. And we have not calculated anything yet, we have not displayed anything.
  • Run it at 60 frames per second, and you will receive 9 million calls . I think you can find the problem here.
  • Stop passing strings in these function calls too.

To get better performance, remove the OOP stuff and go for the ugly spaghetti code, only where it makes sense.

Collision detection can be optimized without testing each particle against each other. Just find the quadrants. It is not so difficult to implement, and its foundations can be used to create a custom solution.

Since you are doing quite some vector math, try the glmatrix library . Optimized vector math :-)

+1
source

You can use FabricJS Canvas Library. FabricJS by default supports interactivity, when you create a new object (circle, rectangle, etc.), you can manipulate it with the mouse or touch screen.

 var canvas = new fabric.Canvas('c'); var rect = new fabric.Rect({ width: 10, height: 20, left: 100, top: 100, fill: 'yellow', angle: 30 }); canvas.add(rect); 

See, we work there in an object-oriented way.

+2
source

I don’t know what significant improvement you can make here, besides switching to technology using hardware acceleration.

Hope this helps a bit, although as noted in the comments, WebGL will be faster. If you don’t know where to start, here is a good one: webglacademy

Nevertheless, I saw a few little things:

 radialWave : function(particle, pIndex, particles){ // As you don't use distance here remove this line // it a really greedy calculus that involves square root // always avoid if you don't have to use it // var distance = getDistance(particle, this.center); if (particle.radius >= (this.dim * 0.0085)) { particle.radiusOper = -0.02; } else if (particle.radius <= 1) { particle.radiusOper = 0.02; } particle.radius += particle.radiusOper * particle.radius; }, 

Another little thing:

 Jarvis.prototype.backgroundDraw = function() { // particles var that = this; // Declare callbacks outside of forEach calls // it will save you a function declaration each time you loop // Do this for logo particles var logoMotionCallback = function(motionType, motionIndex){ // Another improvement may be to use a direct function that does not use 'this' // and instead pass this with a parameter called currentParticle for example // call and apply are known to be pretty heavy -> see if you can avoid this that.motion[motionType].call(that, particle, i, that.logoParticles, "foregroundParticles"); }; var logoFxCallback = function(fxType, fxIndex){ that.fx[fxType].call(that, particle, i, that.logoParticles, "foregroundParticles"); }; var logoCollisionCallback = function(collisionType, collisionIndex){ that.collision[collisionType].call(that, particle, i, that.logoParticles, "foregroundParticles"); }; this.logoParticles.forEach(function(particle, i){ particle.draw(that.backgroundCtx); that.logoParticles.motion.forEach(motionCallback); that.logoParticles.fx.forEach(fxCallback); that.logoParticles.collision.forEach(collisionCallback); }); // Now do the same for background particles var bgMotionCallback = function(motionType, motionIndex){ that.motion[motionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles"); }; var bgFxCallback = function(fxType, fxIndex){ that.fx[fxType].call(that, particle, i, that.backgroundParticles, "backgroundParticles"); }; var bgCollisionCallback = function(collisionType, collisionIndex){ that.collision[collisionType].call(that, particle, i, that.backgroundParticles, "backgroundParticles"); }; this.backgroundParticles.forEach(function(particle, i){ particle.draw(that.backgroundCtx); that.backgroundParticles.motion.forEach(bgMotionCallback); that.backgroundParticles.fx.forEach(bgFxCallback); that.backgroundParticles.collision.forEach(bgCollisionCallback); }); } 
+1
source

I think you may find that webworker support is roughly equal to WebGL support:

WebGL Support: http://caniuse.com/#search=webgl
WebWorker Support: http://caniuse.com/#search=webworker

On the surface, they may seem different, but in fact it is not. The only thing you get is IE10 support temporarily. IE11 has already surpassed IE10 in the market, and the gap will continue to grow. The only thing to note is that webgl support is also based on updated graphics card drivers.

Of course, I do not know your specific needs, so perhaps this will not work.

Functions

Wait what? 200 items on screen slower?

Do less on canvas and do cool stuff in WebGL

Many libraries do this. The canvas should be useful and a little cool. WebGL typically has all the cool particle features.

Webworkers

You will most likely have to use a deferred library or create a system that will determine when all webmasters will be executed and have a pool of workflows.

Some reservations:

  • You cannot access anything from your main application and must communicate through events
  • Items transferred through a web artist are not copied together
  • Setting up webmasters without a separate script may require some research

Unconfirmed rumor: I heard that there is a limited amount of data that you can transfer through the messages of web workers. You should test this, as it seems directly applicable to your use case.

+1
source

Source: https://habr.com/ru/post/977584/


All Articles