Animation

Animation in JavaScript

JavaScript can be used to make animations, for instance on a HTML <canvas>. A JavaScript animation can be seen as an iteration or recursion: repeatedly calling a function that produces a frame on the screen. The function changes, for instance, an HTML element's CSS property a tiny little bit every frame, giving the impression of a moving figure.

We know that the JavaScript engine executes code as fast as possible. Moreover, while the engine executes a task in the event loop, there will be no repainting of graphics on the screen. The repaint will only take place after the task is complete. Therefore, simply calling the animation function in a loop or recursively will only show the last produced frame on the screen and not an animation at all.

Scheduling calls

Executing a callback function, after a timeout being fired, is a separate task in the event loop. Making this callback function produce a frame, will invoke a new screen repaint right after each execution. Doing this recursively will result in an animation:


<style>
#box {
  border-left: 25px solid Fuchsia;
  border-right: 25px solid Gold;
  border-bottom: 25px solid Tomato;
  border-top: 25px solid Aqua;
  display: inline-block;
}
</style>

<div id="box"></div>

<script>
const box = document.getElementById('box');
const timeIncr = 1000/60; // 60 fps
const angleIncr = 1;
let angle = 0;

function animateTimeoutRecursive(){
  angle += angleIncr;
  box.style.transform = "rotate("+(angle%360)+"deg)";	
  setTimeout(animateTimeoutRecursive, timeIncr);
};

animateTimeoutRecursive();
</script>

Result:

The recursion in the example above has no base case, in other words, this animation continues until the entire application (the web page) closes. The time increment, or delay, is set to 1000/60 (in milliseconds), which means that animateTimeoutRecursive produces 60 frames per second (60 fps).

Next to setTimeout() JavaScript also provides setInterval() to schedule a call. Method setInterval() repeatedly calls a function with a fixed time delay (in milliseconds) between each call. This way we do not need explicit recursion (or iteration). The next example does the same thing as the previous example.


const box = document.getElementById('exampleSetIntervalBox');
const timeIncr = 1000/60; // 60 fps
const angleIncr = 1;
let angle = 0;

setInterval(function() {
  angle += angleIncr;
  box.style.transform = "rotate("+(angle%360)+"deg)";
}, timeIncr);

An other advantage of setInterval() over setTimeout() is that one setInterval() function can easily call multiple different animation functions. Multiple separate setIntervals or setTimeouts, each for a separate animation on a page, can consume quite some CPU, even if they all have the same delay time. They all have their own different starting time, depending on where they appear in the code, so the engine needs to keep track of all timings that run simultaneously, but with mutual offsets. In the next example, several animations share the same setInterval, which is CPU friendlier than independent setIntervals for each animation.


setInterval(function() {
  animate1();
  animate2();
  animate3();
}, 1000/60);

setInterval returns an ID that can be passed to clearInterval() to cancel the interval and stop the animation. This is demonstrated in the next example:


<style>
#box { width:50px; height:50px; border-radius:50%; }
.cherry { background-color: #CD001A; }
.berry { background-color: #4F86F7; }
</style>

<div id="box"></div>
<button id="start">Start</button>
<button id="stop">Stop</button>

<script>
let myInterval;
const boxElem = document.getElementById("box");

function startAnimation() {  
  if (!myInterval) { // check if an interval is already running
    changeColorBox();
    myInterval = setInterval(changeColorBox, 1000);
  }
};

function stopAnimation() {
  clearInterval(myInterval);  
  myInterval = null; // release the setInterval from the variable
};

function changeColorBox() {
  boxElem.className = (boxElem.className === "cherry") ? "berry" : "cherry";
};

document.getElementById("start").addEventListener("click", startAnimation);
document.getElementById("stop").addEventListener("click", stopAnimation);
</script>

requestAnimationFrame

An animation generally does not need to always run and does not need to always run at the exact given number of frames per minute. An animation running in background browser tabs, running on a part of the page that is not visible on the screen, or otherwise hidden for the user, may as well be paused. An animation running on a device low on battery or temporarily dealing with heavy CPU or memory usage, may (temporarily) be slowed down (less fps).

Such optimized animations cannot be created by using setTimeout() or setInterval(). Modern JavaScript provides a window.requestAnimationFrame() method to perform animations, but that also lets the browser to optimize animations based on the circumstances such as mentioned above. Concurrent animations using requestAnimationFrame() will also be grouped together into a single screen repaint cycle.

The simplest possible example using requestAnimationFrame():


const box = document.getElementById('box');
const angleIncr = 1;
let angle = 0;

function animation() {
  angle += angleIncr;
  box.style.transform = "rotate("+(angle%360)+"deg)";
  requestAnimationFrame(animation);
};
requestAnimationFrame(animation);

Result:

PS: Note that a requestAnimationFrame() animation is set up using recursion.

Animation speed

The refresh rate of a display is the number of times per second that the screen refreshes. The number of frames per second produced by requestAnimationFrame() will generally match this display refresh rate in most web browsers. This allows animation repaints to be aligned with when the screen refreshes actually occur.

A typical PC monitor will have a refresh rate of 60 Hz (60 Hertz), meaning the display will update 60 times per second. So generally a requestAnimationFrame() animation runs at 60 fps. However, gaming displays or other fancy displays may have much higher refresh rates. This means that the box in the example above will rotate much faster on a 240 Hz screen than on a 60 Hz screen. To get a more uniform animation on all devices under all circumstances, we generally need to include a way to get a more defined "animation speed".

So, the callback function in requestAnimationFrame(callback) is called a number of times per second generally equal to the display refresh rate, unless circumstances like heavy CPU load or low battery demand less fps. This callback is passed one single argument, the timestamp, indicating the time passed (in milliseconds) from the point in time when requestAnimationFrame(callback) was first called. This way we can calculate the time elapsed between two successive frame updates.

An fps independent animation speed is an increment (like 0.1 degrees rotation or 2 pixels translation) that we want the animation to change every millisecond. Let this fps independent animation speed be s (increment per millisecond). Then, let t (milliseconds per frame) be the time elapsed between two successive frame updates, calculated based on the timestamp (elapsedPerFrame = timestamp - previousTimeStamp). Then, for each frame, we multiply this calculated elapsed time by the desired s. Written as a formula: i [px/frame] = t [ms/frame] × s [px/ms] (with, in this example, the increment in pixels). So now for each frame we have an increment (i) that we want the animation to change. The animation speed (s) is now fps independent. With a lower fps, the fps depended increments per frame (i) will increase (and vice versa), but the animation speed will remain the same. In JavaScript (with angleIncr = s in degrees per millisecond):


const box = document.getElementById('box');
const angleIncr = 0.1; // 0.1 degrees per millisecond (100 degrees per second)
let angle = 0;
let previousTimeStamp;

function animation(timestamp) {
  if (previousTimeStamp === undefined) {
	previousTimeStamp = timestamp;
  }
  const elapsedPerFrame = timestamp - previousTimeStamp; //	elapsed time per frame in milliseconds.
	
  angle += (elapsedPerFrame * angleIncr); // increment = elapsedPerFrame * angleIncr
  box.style.transform = "rotate("+(angle%360)+"deg)";
  
  previousTimeStamp = timestamp;
  requestAnimationFrame(animation);
};
requestAnimationFrame(animation);

Result:

The figure rotates 0.1 degrees per millisecond (100 degrees per second).

Animation time

We can also use the timestamp to calculate the total elapsed time after every frame. We can use this elapsed time to animate things in specific time intervals, for instance in an image carousel or slide show, replacing the image every few seconds. Next example colors a next square every 2 seconds.


const t = 2000; // 2 seconds
const boxes = document.getElementById("box_container").children; // 3 boxes
let start, n, i = 0;

function animation(timestamp) {
  if (start === undefined) {
    start = timestamp;
  }
  const totalElapsed = timestamp - start;		
  if (totalElapsed > t) {
    n = i % boxes.length; // % = Modulus aka division remainder || 0/3=0r0, 1/3=0r1, 2/3=0r2, 3/3=1r0, 4/3=1r1 ...
    boxes[n].style.background = "#999";	// back to gray
    n = ++i % boxes.length;
    boxes[n].style.background = "#ff0000"; // next red

    start = timestamp;
    requestAnimationFrame(animation);
  } else {	
    requestAnimationFrame(animation);
  }	
};
requestAnimationFrame(animation);

Result:

Start and stop

In the rotating square example above we can write angle += (elapsedPerFrame * angleIncr) as angle = totalElapsed * angleIncr (see below).

a += (tn × s) ⇒
a = an-1 + (tn × s) ⇒
a = s × (t0 + t1 + ... + tn) ⇒
a = s × ttotal

We can use totalElapsed to stop the animation after a certain time elapsed since the start of the animation (in milliseconds). Next JavaScript does exactly the same as the rotating square example, except now the animation stops after 2 seconds:


const box = document.getElementById('box');
const angleIncr = 0.1;
let angle = 0;
let start;

function animation(timestamp) {
  if (start === undefined) {
    start = timestamp;
  }
  const totalElapsed = timestamp - start;	
  
  angle = totalElapsed * angleIncr;
  box.style.transform = "rotate("+(angle%360)+"deg)";
	  
  if (totalElapsed < 2000) { requestAnimationFrame(animation); }  // Stop the animation after 2 seconds
};
requestAnimationFrame(animation);

We can also let the animation stop at a certain number of degrees:


const box = document.getElementById('box');
const angleIncr = 0.1;
const stopAngle = 360;
let angle = 0;
let start;

function animation(timestamp) {
  if (start === undefined) {
    start = timestamp;
  }
  const totalElapsed = timestamp - start;	
  
  // Math.min() is used here to make sure the element stops at exactly stopAngle deg
  angle = Math.min(totalElapsed * angleIncr, stopAngle);
  box.style.transform = "rotate("+(angle%360)+"deg)";
	  
  if (angle !== stopAngle) { requestAnimationFrame(animation); }
};
requestAnimationFrame(animation);

In an animation we often want some movement in a certain time. Then we do not have to worry about the size of the increments: this size automatically follows from the two given inputs. Next example moves a box 350 pixels to the right in 1.5 seconds.


const duration = 1500; // Animation takes 1500 milliseconds
const totalMovement = 350; // The box moves 350 pixels
const box = document.getElementById('box');
let start;

function animation(timestamp) {
  if (start === undefined) {
	start = timestamp;
  }
  // Divide the elapsed time by the duration to always get a number between 0 and 1:
  const elapsedPerDuration = Math.min((timestamp - start) / duration, 1);
  const progress = elapsedPerDuration * totalMovement;
  box.style.transform = "translate("+progress+"px)";	  
  if (elapsedPerDuration < 1) { requestAnimationFrame(animation); }
};
requestAnimationFrame(animation);

Result:

requestAnimationFrame returns an ID that can be passed to cancelAnimationFrame() to explicitly cancel the animation. This works similar to how setInterval can be canceled.


const box = document.getElementById("box");

const angleIncr = 0.1;
let angle = 0;
let previousTimeStamp;
let myAnim;

function animation(timestamp) {
  if (previousTimeStamp === null || previousTimeStamp === undefined) {
    previousTimeStamp = timestamp;
  }
  const elapsed = timestamp - previousTimeStamp;	  
  angle += (elapsed * angleIncr);
  box.style.transform = "rotate("+(angle%360)+"deg)";
  previousTimeStamp = timestamp;	  
  myAnim = requestAnimationFrame(animation);
};

function startAnimation() {  
  if(!myAnim) { myAnim = requestAnimationFrame(animation); }
};	

function stopAnimation() {
  if(myAnim) {
    cancelAnimationFrame(myAnim);  
    myAnim = null;
    previousTimeStamp = null;
  }
};

document.getElementById("start").addEventListener("click", startAnimation);
document.getElementById("stop").addEventListener("click", stopAnimation);

Result:

PS: Note that in the above example it is more user friendly to have both buttons combined into one "pause" button.

Examples

SVG animation


<svg role="img" aria-label="An SVG image that is being animated"
  xmlns="http://www.w3.org/2000/svg" height="100%" width="100%" viewBox="0 0 53 11">
  <g id="container">
    <path class="SVGletter" stroke="MediumVioletRed" d="m 0.92578125,8.4807348 0,-7.1582032 0.94726565,0 0,2.9394532 3.7207031,0 0,-2.9394532 0.9472656,0 0,7.1582032 -0.9472656,0 0,-3.3740235 -3.7207031,0 0,3.3740235 z" />
    <path class="SVGletter" stroke="BlueViolet" d="m 11.560547,6.8108129 0.908203,0.1123047 c -0.143234,0.5306 -0.408533,0.9423834 -0.795898,1.2353515 -0.387374,0.2929688 -0.882165,0.439453 -1.484375,0.4394532 C 9.4300109,8.5979221 8.8286118,8.3643612 8.3842773,7.8972387 7.9399408,7.4301173 7.7177731,6.7750073 7.7177734,5.9319066 c -3e-7,-0.8723924 0.2246088,-1.549475 0.6738282,-2.03125 0.4492172,-0.4817657 1.0318989,-0.7226509 1.7480464,-0.7226562 0.693356,5.3e-6 1.259762,0.2360077 1.699219,0.7080078 0.439448,0.4720093 0.659175,1.1360712 0.65918,1.9921875 -5e-6,0.052086 -0.0016,0.1302108 -0.0049,0.234375 l -3.8671874,0 C 8.6585273,6.682234 8.81966,7.1184314 9.109375,7.4211645 c 0.2897115,0.3027351 0.6510393,0.4541021 1.083984,0.4541015 0.322263,6e-7 0.597328,-0.084635 0.825196,-0.2539062 0.22786,-0.1692698 0.408524,-0.4394519 0.541992,-0.8105469 z m -2.8857423,-1.4208984 2.8955083,0 C 11.531246,4.9537201 11.420569,4.626572 11.238281,4.4084691 10.95833,4.0699319 10.595374,3.9006612 10.149414,3.9006566 9.7457658,3.9006612 9.4064107,4.0357522 9.1313477,4.3059301 8.856281,4.5761163 8.7041002,4.937444 8.6748047,5.3899145 z" />
    <path class="SVGletter" stroke="Chartreuse" d="m 13.557617,8.4807348 0,-7.1582032 0.878906,0 0,7.1582032 z" />
    <path class="SVGletter" stroke="Crimson" d="m 15.78418,8.4807348 0,-7.1582032 0.878906,0 0,7.1582032 z" />
    <path class="SVGletter" stroke="MediumTurquoise" d="m 17.703125,5.8879613 c 0,-0.9602829 0.266926,-1.6715452 0.800781,-2.133789 0.445962,-0.3841095 0.989581,-0.5761666 1.63086,-0.5761719 0.712887,5.3e-6 1.295568,0.2335663 1.748046,0.7006836 0.452469,0.4671265 0.678706,1.1124709 0.678711,1.9360351 -5e-6,0.6673197 -0.100102,1.1922216 -0.300293,1.5747071 -0.2002,0.3824877 -0.49154,0.6795251 -0.874023,0.8911133 C 21.004716,8.492128 20.587236,8.5979221 20.134766,8.5979223 19.408852,8.5979221 18.822101,8.365175 18.374512,7.8996801 17.92692,7.4341863 17.703125,6.7636141 17.703125,5.8879613 z m 0.90332,0 c -1e-6,0.6640645 0.144856,1.161297 0.434571,1.4916992 0.289711,0.3304045 0.654294,0.4956061 1.09375,0.4956055 0.436194,6e-7 0.79915,-0.1660148 1.088867,-0.4980469 0.289709,-0.3320298 0.434566,-0.8382142 0.43457,-1.5185546 -4e-6,-0.6412728 -0.145675,-1.1271122 -0.437012,-1.4575196 -0.291344,-0.3303992 -0.653486,-0.4956009 -1.086425,-0.4956054 -0.439456,4.5e-6 -0.804039,0.1643924 -1.09375,0.493164 -0.289715,0.3287798 -0.434572,0.8251986 -0.434571,1.4892578 z" />
    <path class="SVGletter" stroke="DarkOliveGreen" d="m 27.327148,8.4807348 -1.586914,-5.1855469 0.908203,0 0.825196,2.9931641 0.307617,1.1132812 c 0.01302,-0.055337 0.102537,-0.4117824 0.268555,-1.0693359 L 28.875,3.2951879 l 0.90332,0 0.776367,3.0078125 0.25879,0.9912109 L 31.111328,6.2932348 32,3.2951879 l 0.854492,0 -1.621094,5.1855469 -0.913086,0 L 29.495117,5.375266 29.294922,4.491477 28.245117,8.4807348 z" />
    <path class="SVGletter" stroke="OrangeRed" d="m 33.269531,5.8879613 c 0,-0.9602829 0.266927,-1.6715452 0.800781,-2.133789 0.445962,-0.3841095 0.989582,-0.5761666 1.63086,-0.5761719 0.712887,5.3e-6 1.295569,0.2335663 1.748047,0.7006836 0.452469,0.4671265 0.678705,1.1124709 0.678711,1.9360351 -6e-6,0.6673197 -0.100103,1.1922216 -0.300293,1.5747071 -0.2002,0.3824877 -0.491541,0.6795251 -0.874024,0.8911133 -0.38249,0.2115885 -0.79997,0.3173826 -1.252441,0.3173828 -0.725914,-2e-7 -1.312664,-0.2327473 -1.760254,-0.6982422 -0.447592,-0.4654938 -0.671387,-1.136066 -0.671387,-2.0117188 z m 0.903321,0 c -2e-6,0.6640645 0.144855,1.161297 0.43457,1.4916992 0.289711,0.3304045 0.654294,0.4956061 1.09375,0.4956055 0.436195,6e-7 0.79915,-0.1660148 1.088867,-0.4980469 0.289709,-0.3320298 0.434566,-0.8382142 0.43457,-1.5185546 -4e-6,-0.6412728 -0.145674,-1.1271122 -0.437011,-1.4575196 -0.291345,-0.3303992 -0.653487,-0.4956009 -1.086426,-0.4956054 -0.439456,4.5e-6 -0.804039,0.1643924 -1.09375,0.493164 -0.289715,0.3287798 -0.434572,0.8251986 -0.43457,1.4892578 z" />
    <path class="SVGletter" stroke="Red" d="m 39.15332,8.4807348 0,-5.1855469 0.791016,0 0,0.7861328 c 0.201821,-0.3678338 0.388182,-0.6103465 0.559082,-0.7275391 0.170896,-0.1171822 0.358884,-0.1757759 0.563965,-0.1757812 0.296221,5.3e-6 0.597327,0.094406 0.90332,0.2832031 l -0.302734,0.8154297 c -0.214847,-0.1269488 -0.42969,-0.1904253 -0.644532,-0.1904297 -0.192059,4.4e-6 -0.364585,0.057784 -0.517578,0.1733399 -0.152996,0.115564 -0.262046,0.2758828 -0.327148,0.480957 -0.09766,0.3125034 -0.146486,0.6543 -0.146484,1.0253906 l 0,2.7148438 z" />
    <path class="SVGletter" stroke="Teal" d="m 42.483398,8.4807348 0,-7.1582032 0.878907,0 0,7.1582032 z" />
    <path class="SVGletter" stroke="Yellow" d="m 48.09375,8.4807348 0,-0.6542969 C 47.76497,8.3407609 47.281572,8.5979221 46.643555,8.5979223 46.230141,8.5979221 45.850096,8.48399 45.503418,8.2561254 45.156737,8.0282613 44.888183,7.710065 44.697754,7.3015355 44.507324,6.8930085 44.412109,6.4234452 44.412109,5.8928441 c 0,-0.517575 0.08626,-0.9871383 0.258789,-1.4086914 0.172526,-0.421545 0.431315,-0.7446241 0.776368,-0.9692382 0.34505,-0.2246042 0.730792,-0.3369088 1.157226,-0.3369141 0.312497,5.3e-6 0.590817,0.065923 0.834961,0.1977539 0.244137,0.1318409 0.442705,0.303553 0.595703,0.5151367 l 0,-2.5683594 0.874024,0 0,7.1582032 z M 45.31543,5.8928441 c -2e-6,0.6640645 0.139972,1.1604832 0.419922,1.4892579 0.279946,0.3287768 0.610349,0.4931646 0.99121,0.493164 0.384112,6e-7 0.710446,-0.157063 0.979004,-0.4711914 0.268551,-0.3141262 0.402828,-0.7934552 0.402832,-1.4379883 -4e-6,-0.7096322 -0.136722,-1.230465 -0.410156,-1.5625 C 47.424801,4.0715595 47.087888,3.905544 46.6875,3.9055395 46.296873,3.905544 45.970538,4.0650491 45.708496,4.3840551 45.44645,4.7030693 45.315428,5.2059985 45.31543,5.8928441 z" />
    <path class="SVGletter" stroke="SpringGreen" d="m 50.764648,6.703391 -0.268554,-3.7939453 0,-1.5869141 1.088867,0 0,1.5869141 -0.253906,3.7939453 z m -0.229492,1.7773438 0,-1.0009766 1.010742,0 0,1.0009766 z" />
  </g>
</svg>

<style>
.SVGletter {
visibility:hidden;
fill:none;
fill-opacity:1;
stroke-width:0.5;
stroke-opacity:1;
stroke-linecap:round;
stroke-linejoin:round;
stroke-miterlimit:4;
}
</style>

<script>
const offsetIncr = 0.03;	
let paths = document.querySelectorAll("#container path");
let previousTimeStamp, length, elem, i;	 	

requestAnimationFrame(animate);

function animate(timestamp) {
  if (previousTimeStamp === null || previousTimeStamp === undefined) {
	previousTimeStamp = timestamp;
  }
  const elapsed = timestamp - previousTimeStamp;	
  previousTimeStamp = timestamp;

  if (length && (length - i) >= 0) {
	i += (elapsed * offsetIncr);
	elem.style.strokeDashoffset =  length - i;
	if ((length - i) <= 0) { elem.style.strokeDasharray = "0"; }		
	requestAnimationFrame(animate);
  } else if (paths.length) {
	[elem, ...paths] = paths; // Array destructuring (see remark)		
	length = elem.getTotalLength();		  
	elem.style.strokeDasharray = length + ' ' + length;
	elem.style.strokeDashoffset = length;
	elem.style.visibility="visible";
	i = 0;
	requestAnimationFrame(animate);
  } else { console.log("animation is done") }		
};
</script>

BTW. The restart button and random color of the letters is not included in the above code.

BTW. The above example uses array destructuring, which will be covered later this tutorial.

HTML canvas animation, with timing

Next example is a HTML canvas version of the earlier shown "some movement in a certain time" example, but now with timing. Timing or easing functions are functions that let the animation progress in other ways that just linear, giving all sorts of effects to the animation. There are a lot of ways to get some kind of timing. There are also JavaScript libraries, such as Anime.js, that provide lots of animation features, such as easing.


	const myCanvas = document.getElementById('myCanvas');
	const ctx = myCanvas.getContext("2d");
	
	drawBox(0,0);	
	myCanvas.onclick = function() {	  
	  animation({
	    duration: 2000,
		finalState: 350,
		easing: easeOut,
		createFrame: createFrame
	  });
	};	
	
	function animation(settings) {
	  let start, myAnim;
	  if(myAnim) { cancelAnimationFrame(myAnim) }
	  myAnim = requestAnimationFrame(function animate(timestamp) {	  
	    if (start === undefined) {
	    start = timestamp;
	    }
	    const elapsedPerDuration = Math.min((timestamp - start) / settings.duration, 1);
	    const progress = settings.easing(elapsedPerDuration) * settings.finalState;
	    settings.createFrame(progress);	  
	    if (elapsedPerDuration < 1) { myAnim = requestAnimationFrame(animate); }	  
	  });	  
	};		

	function createFrame(progress) {
	  // The canvas needs to be cleared before drawing the new frame:
	  ctx.clearRect(0, 0, myCanvas.width, myCanvas.height);
	  drawBox(progress, 0);
	};
	
	function drawBox(x,y) {
	  const grd = ctx.createLinearGradient(x, y, x+35, y+35);
	  grd.addColorStop(0, "MediumVioletRed");
	  grd.addColorStop(1, "OliveDrab");
	  ctx.fillStyle = grd;
	  ctx.fillRect(x, y, 35, 35);
	};
	
	const easeOut = function bounce(t) {
	  const n1 = 7.5625
	  const d1 = 2.75
	  if (t < 1 / d1) {
		return n1 * t * t
	  } else if (t < 2 / d1) {
		return n1 * (t -= 1.5 / d1) * t + 0.75
	  } else if (t < 2.5 / d1) {
		return n1 * (t -= 2.25 / d1) * t + 0.9375
	  } else {
		return n1 * (t -= 2.625 / d1) * t + 0.984375
	  }
	};

PS. Credit to easings.net for the ease out function.

WebGL animation

JavaScript can make use of an API for rendering high-performance interactive 3D (and 2D) graphics called the WebGL API. These graphics can also be animated by using the techniques as described before. The next example is a simple animation of a moving icosahedron, rendered with WebGL, using the three.js library. In this example Perlin noise is applied to the animation increments to get a smooth random movement.