Using Canvas to animate a sorting algorithm in JS
For fun I am trying to create a visualization of different sorting algorithms, but I've run into an issue with Canvas animations.
I assumed I would just be able to call a draw function within the sorter method, but this causes the browser to lock up until the array is fully sorted and then draws some middle frame.
How would I go about animating from within the sorting methods? Below is the code I have so far, I wouldn't run this snippet as it will hang the tab for a few seconds.
N = 250; // Array Size
XYs = 5; // Element Visual Size
Xp = 1; // Start Pos X
Yp = 1; // Start Pos Y
var canvas;
var l = Array.apply(null, {
length: N
}).map(Number.call, Number);
Array.prototype.shuffle = function() {
var i = this.length,
j, temp;
if (i == 0) return this;
while (--i) {
j = Math.floor(Math.random() * (i + 1));
temp = this[i];
this[i] = this[j];
this[j] = temp;
}
return this;
}
function map_range(x, in_min, in_max, out_min, out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
function rainbow(x) {
var m = map_range(x, 0, N, 0, 359);
return 'hsl(' + m + ',100%,50%)';
}
function init() {
canvas = document.getElementById('canvas');
l.shuffle();
draw();
bubbleSort(l);
}
function draw() {
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
for (var i = 0; i < l.length; i++) {
ctx.fillStyle = rainbow(l[i]);
ctx.fillRect((Xp * i) * XYs, Yp * XYs, XYs, XYs);
}
}
}
function bubbleSort(a) {
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
draw();
setTimeout(function() {}, 10);
}
}
} while (swapped);
}
<html>
<body onload="init();">
<canvas id="canvas" width="1500" height="1500"></canvas>
</body>
</html>
Solution 1:
One solution is the ES6 Generator function*
along with its yield
statement.
That allows you to pause a function and restart it later where it has been paused:
N = 100; // Array Size
XYs = 5; // Element Visual Size
Xp = 1; // Start Pos X
Yp = 1; // Start Pos Y
var canvas;
var l = Array.apply(null, {
length: N
}).map(Number.call, Number);
Array.prototype.shuffle = function() {
var i = this.length,
j, temp;
if (i == 0) return this;
while (--i) {
j = Math.floor(Math.random() * (i + 1));
temp = this[i];
this[i] = this[j];
this[j] = temp;
}
return this;
}
function map_range(x, in_min, in_max, out_min, out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
function rainbow(x) {
var m = map_range(x, 0, N, 0, 359);
return 'hsl(' + m + ',100%,50%)';
}
function init() {
canvas = document.getElementById('canvas');
l.shuffle();
var sort = bubbleSort(l);
// an anim function triggered every 60th of a second
function anim(){
requestAnimationFrame(anim);
draw();
sort.next(); // call next iteration of the bubbleSort function
}
anim();
}
function draw() {
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
for (var i = 0; i < l.length; i++) {
ctx.fillStyle = rainbow(l[i]);
ctx.fillRect((Xp * i) * XYs, Yp * XYs, XYs, XYs);
}
}
}
function* bubbleSort(a) { // * is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
yield swapped; // pause here
}
}
} while (swapped);
}
init();
<canvas id="canvas" width="500" height="20">
You can also now use async/await
for a more readable code achieving the same goal:
const N = 100; // Array Size
const XYs = 5; // Element Visual Size
const Xp = 1; // Start Pos X
const Yp = 1; // Start Pos Y
let canvas;
const l = Array.from({ length: N }, (_,i) => i);
Array.prototype.shuffle = function() {
let i = this.length;
if (i == 0) return this;
while (--i) {
const j = Math.floor(Math.random() * (i + 1));
const temp = this[i];
this[i] = this[j];
this[j] = temp;
}
return this;
}
function map_range(x, in_min, in_max, out_min, out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
function rainbow(x) {
const m = map_range(x, 0, N, 0, 359);
return 'hsl(' + m + ',100%,50%)';
}
function init() {
canvas = document.getElementById('canvas');
l.shuffle();
bubbleSort(l);
}
function draw() {
if (canvas.getContext) {
const ctx = canvas.getContext('2d');
for (let i = 0; i < l.length; i++) {
ctx.fillStyle = rainbow(l[i]);
ctx.fillRect((Xp * i) * XYs, Yp * XYs, XYs, XYs);
}
}
}
function drawNextFrame() {
return new Promise((res) => requestAnimationFrame(res))
.then(draw);
}
async function bubbleSort(a) { // async is magic
let swapped;
do {
swapped = false;
for (let i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
await drawNextFrame(); // pause here
}
}
} while (swapped);
}
init();
<canvas id="canvas" width="500" height="20">
Solution 2:
You need a render loop. requestAnimationFrame() is your friend here. With this method you give the browser a callback that is called before the next frame draw.
Within that callback you draw your stuff and call requestAnimationFrame() again with your same callback like this:
requestAnimationFrame(renderLoop);
function renderLoop()
{
// visualize your array `l` at this point
// call the render callback for the next frame draw
requestAnimationFrame(renderLoop);
}
https://developer.mozilla.org/de/docs/Web/API/window/requestAnimationFrame
You get a smooth animation with usually a framerate of 60 fps.
For integrating that approach in your code:
-
Delete the lines
draw(); setTimeout(function() {}, 10);
-
call the initial
requestAnimationFrame(renderLoop);
when your program start.