Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p><strong>Updated</strong></p> <h2>Some techniques that can be applied</h2> <p>Here are some optimization techniques that can be applied to make this work more fluent in FF and Safari as well.</p> <p>That being said: Chrome's canvas implementation is very good and much faster (at the moment) than the bone provided by Firefox and Safari. The new Opera uses the same engine as Chrome and is (about?) equally as fast as Chrome's.</p> <p>For this to work fine cross-browser some compromises needs to be made and as always <strong>quality</strong> will suffer.</p> <p>The techniques I try to demonstrate are:</p> <ul> <li>Cache a single gradient that is used as meta ball basis</li> <li>Cache everything if possible</li> <li>Render in half resolution</li> <li>Use <code>drawImage()</code> to update main canvas</li> <li>Disable image smoothing</li> <li>Use integer coordinates and sizes</li> <li>Use <code>requestAnimationFrame()</code></li> <li>Use <code>while</code> loops as often as you can</li> </ul> <h2>Bottlenecks</h2> <p>There is a high cost in generating a gradient for each metaball. So when we cache this once and for all we will just by doing that notice a huge improvement in performance.</p> <p>The other point is <code>getImageData</code> and <code>putImageData</code> and the fact that we need to use a high-level language to iterate over a low-level byte array. Fortunately the array is typed array so that helps a little but we won't be able to get much more out of it unless we sacrifice more quality.</p> <p>When you need to squeeze everything you can the so-called micro-optimizations becomes vital (these has an undeserved bad reputation IMO).</p> <p>From the impression of your post: You seem to be very close to have this working but from the provided code I cannot see what went wrong so-to-speak.</p> <p>In any case - Here is an actual implementation of this (based on the code you refer to):</p> <p><strong><a href="http://jsfiddle.net/epistemex/ucumbskf/" rel="nofollow">Fiddle demo</a></strong></p> <p>Pre-calculate variables in the initial steps - everything we can pre-calculate helps us later as we can use the value directly:</p> <pre><code>var ..., // multiplicator for resolution (see comment below) factor = 2, width = 500, height = 500, // some dimension pre-calculations widthF = width / factor, heightF = height / factor, // for the pixel alpha threshold = 210, thresholdQ = threshold * 0.25, // for gradient (more for simply setting the resolution) grad, dia = 500 / factor, radius = dia * 0.5, ... </code></pre> <p>We use a factor here to reduce the actual size and to scale the final render to on-screen canvas. For each 2 factor you save 4x pixels exponentially. I preset this to 2 in the demo and this works great with Chrome and good with Firefox. You might even be able to run factor of 1 (1:1 ratio) in both browsers on a better spec'ed machine than mine (Atom CPU).</p> <p><strong>Init the sizes of the various canvases:</strong></p> <pre><code>// set sizes on canvases canvas.width = width; canvas.height = height; // off-screen canvas tmpCanvas.width = widthF; tmpCanvas.height = heightF; // gradient canvas gCanvas.width = gCanvas.height = dia </code></pre> <p>Then generate a single instance of a gradient that will be cached for the other balls later. Worth to notice: I initially used only this to draw all the balls but later decided to cache each ball as an image (canvas) instead of drawing and scaling.</p> <p>This has a memory penalty but increases the performance. If memory is of importance you can skip the caching of rendered balls in the loop that generates them and just <code>drawImage</code> the gradient canvas instead when you need to draw the balls.</p> <p><strong>Generate gradient:</strong></p> <pre><code>var grad = gCtx.createRadialGradient(radius, radius, 1, radius, radius, radius); grad.addColorStop(0, 'rgba(0,0,255,1)'); grad.addColorStop(1, 'rgba(0,0,255,0)'); gCtx.fillStyle = grad; gCtx.arc(radius, radius, radius, 0, Math.PI * 2); gCtx.fill(); </code></pre> <p>Then in the loop that generates the various metaballs.</p> <p><strong>Cache the calculated and rendered metaball:</strong></p> <pre><code>for (var i = 0; i &lt; 50; i++) { // all values are rounded to integer values var x = Math.random() * width | 0, y = Math.random() * height | 0, vx = Math.round((Math.random() * 8) - 4), vy = Math.round((Math.random() * 8) - 4), size = Math.round((Math.floor(Math.random() * 200) + 200) / factor), // cache this variant as canvas c = document.createElement('canvas'), cc = c.getContext('2d'); // scale and draw the metaball c.width = c.height = size; cc.drawImage(gCanvas, 0, 0, size, size); points.push({ x: x, y: y, vx: vx, vy: vy, size: size, maxX: widthF + size, maxY: heightF + size, ball: c // here we add the cached ball }); } </code></pre> <p>Then we turn off interpolating for images that are being scaled - this gains even more speed.</p> <p>Note that you can also use CSS in some browsers to do the same as here.</p> <p><strong>Disable image smoothing:</strong></p> <pre><code>// disable image smoothing for sake of speed ctx.webkitImageSmoothingEnabled = false; ctx.mozImageSmoothingEnabled = false; ctx.msImageSmoothingEnabled = false; ctx.oImageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false; // future... </code></pre> <p>Now the non-critical parts are done. The rest of the code utilizes these tweaks to perform better.</p> <p>The main loop now looks like this:</p> <pre><code>function animate() { var len = points.length, point; // clear the frame of off-sceen canvas tmpCtx.clearRect(0, 0, width, height); while(len--) { point = points[len]; point.x += point.vx; point.y += point.vy; // the checks are now exclusive so only one of them is processed if (point.x &gt; point.maxX) { point.x = -point.size; } else if (point.x &lt; -point.size) { point.x = point.maxX; } if (point.y &gt; point.maxY) { point.y = -point.size; } else if (point.y &lt; -point.size) { point.y = point.maxY; } // draw cached ball onto off-screen canvas tmpCtx.drawImage(point.ball, point.x, point.y, point.size, point.size); } // trigger levels metabalize(); // low-level loop requestAnimationFrame(animate); } </code></pre> <p>Using <code>requestAnimationFrame</code> squeezes a little more of the browser as it is intended to be more low-level and more efficient than just using a <code>setTimeout</code>.</p> <p>The original code checked for both edges - this is not necessary as a ball can only cross one edge at the time (per axis).</p> <p>The metabolize function is modified like this:</p> <pre><code>function metabalize(){ // cache what can be cached var imageData = tmpCtx.getImageData(0 , 0, widthF, heightF), pix = imageData.data, i = pix.length - 1, p; // using a while loop here instead of for is beneficial while(i &gt; 0) { p = pix[i]; if(p &lt; threshold) { pix[i] = p * 0.1667; // multiply is faster than div if(p &gt; thresholdQ){ pix[i] = 0; } } i -= 4; } // put back data, clear frame and update scaled tmpCtx.putImageData(imageData, 0, 0); ctx.clearRect(0, 0, width, height); ctx.drawImage(tmpCanvas, 0, 0, width, height); } </code></pre> <p>Some micro-optimizations that actually helps in this context.</p> <p>We cache the pixel value for alpha channel as we use it more than two times. Instead of <em>diving</em> on <code>6</code> we <em>multiply</em> with <code>0.1667</code> as multiplication is a tad faster.</p> <p>We have already cached <code>tresholdQ</code> value (25% of <code>threshold</code>). Putting the cached value inside the function would give a little more speed.</p> <p>Unfortunately as this method is based on the alpha channel we need to clear also the main canvas. This has a (relatively) huge penalty in this context. The optimal would be to be able to use solid colors which you could "blit" directly but I didn't look Into that aspect here.</p> <p>You could also had put the point data in an array instead of as objects. However, since there are so few this will probably not be worth it in this case.</p> <h2>In conclusion</h2> <p>I have probably missed one or two (or more) places which can be optimized further but you get the idea.</p> <p>And as you can see the modified code runs several times faster than the original code mainly due to the compromise we make here with quality and some optimizations particularly with the gradient.</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload