Implementing Sorting Algorithm Visualizations with JavaScript and Canvas
This article explains how to create animated visualizations of 50 sorting algorithms using JavaScript and the HTML5 canvas by converting array elements into polar coordinates, randomizing their positions, and redrawing the canvas step‑by‑step during the sorting process, complete with full source code examples.
Introduction
The author was inspired by a video showing 50 sorting algorithm visualizations in Java and decided to implement the same effect using JavaScript and the HTML5 canvas, starting with a simple bubble‑sort animation.
Implementation Idea
Desired Effect
The goal is to transform a sorted array into a set of points on a polar coordinate system, then animate the transition from the initial scattered state to the sorted state.
Polar Coordinates Refresher
Recall the definitions: O is the origin, ρ is the radius, and θ is the angle. The conversion formulas are x = ρ * Math.cos(θ) and y = ρ * Math.sin(θ) .
Mapping Array to Polar Points
For an array of 37 elements, each index is multiplied by 10 to obtain an angle (θ) covering 0°‑360°, while the element value becomes the radius (ρ). The resulting (θ, ρ) pairs are converted to (x, y) coordinates for drawing.
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36];Using the rule above, each element is mapped to a point such as (0 → θ=0°, ρ=0) , (1 → θ=10°, ρ=1) , …, (36 → θ=360°, ρ=36) .
Random Shuffling
To start the animation, a shuffled array is generated. The example creates four random permutations of the numbers 0‑179, giving a total of 720 elements.
let nums = [];
for (let i = 0; i < 4; i++) {
const arr = [...Array(180).keys()];
const res = [];
while (arr.length) {
const randomIndex = Math.random() * arr.length - 1;
res.push(arr.splice(randomIndex, 1)[0]);
}
nums = [...nums, ...res];
}Canvas Setup
A 1000×1000 canvas with a black background is added to the page. The canvas origin is moved to the center (500, 500) using ctx.translate(500, 500) , and the drawing color is set to white.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.translate(500, 500);Drawing Points
Pre‑compute cosine and sine values for all integer angles (0‑359) and store them in an array CosandSin . Then, for each element in the shuffled array, select the appropriate angle (two elements per angle) and compute the Cartesian coordinates.
const CosandSin = [];
for (let i = 0; i < 360; i++) {
const rad = i / 180 * Math.PI;
CosandSin.push({ cos: Math.cos(rad), sin: Math.sin(rad) });
}A simple Rect constructor is used to represent each point as a small rectangle, and a drawAll function renders all rectangles on the canvas.
function Rect(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
Rect.prototype.draw = function () {
ctx.beginPath();
ctx.fillRect(this.x, this.y, this.width, this.height);
ctx.closePath();
};The rendering loop creates a Rect for each element using the pre‑computed cosine/sine values and draws them.
function drawAll(arr) {
const rects = [];
for (let i = 0; i < arr.length; i++) {
const num = arr[i];
const { cos, sin } = CosandSin[Math.floor(i / 2)];
const x = num * cos;
const y = num * sin;
rects.push(new Rect(x, y, 5, 3));
}
rects.forEach(rect => rect.draw());
}
drawAll(nums);Animating the Sort
During sorting, the canvas is cleared with ctx.clearRect(-500, -500, 1000, 1000) , the array is updated, and drawAll is called after each significant step (e.g., after each outer loop iteration). The drawAll function is wrapped in a Promise to allow await delays.
function drawAll(arr) {
return new Promise(resolve => {
setTimeout(() => {
ctx.clearRect(-500, -500, 1000, 1000);
// ... (same rendering code as above)
resolve('draw success');
}, 10);
});
}Sorting Algorithms
Several classic sorting algorithms are implemented, each calling await drawAll(arr) to update the visualization:
Bubble Sort
async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
var temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr);
}
return arr;
}Selection Sort
async function selectionSort(arr) {
var len = arr.length;
for (var i = 0; i < len - 1; i++) {
var minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) minIndex = j;
}
var temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
await drawAll(arr);
}
return arr;
}Insertion Sort
async function insertionSort(arr) {
for (var i = 1; i < arr.length; i++) {
var key = arr[i];
var j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
await drawAll(arr);
}
return arr;
}Heap Sort
async function heapSort(array) {
// build heap
var heapSize = array.length;
for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
heapify(array, i, heapSize);
await drawAll(array);
}
// sort
for (var j = heapSize - 1; j >= 1; j--) {
var temp = array[0];
array[0] = array[j];
array[j] = temp;
heapify(array, 0, --heapSize);
await drawAll(array);
}
return array;
}
function heapify(arr, x, len) {
var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
if (l < len && arr[l] > arr[largest]) largest = l;
if (r < len && arr[r] > arr[largest]) largest = r;
if (largest != x) {
temp = arr[x];
arr[x] = arr[largest];
arr[largest] = temp;
heapify(arr, largest, len);
}
}Quick Sort
async function quickSort(array, left, right) {
if (left < right) {
var pivot = array[right];
var i = left - 1, temp;
for (var j = left; j <= right; j++) {
if (array[j] <= pivot) {
i++;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
await drawAll(array);
await quickSort(array, left, i - 1);
await quickSort(array, i + 1, right);
await drawAll(array);
}
return array;
}Radix Sort
async function radixSort(arr, maxDigit) {
var mod = 10, dev = 1, counter = [];
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for (var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if (counter[bucket] == null) counter[bucket] = [];
counter[bucket].push(arr[j]);
}
var pos = 0;
for (var j = 0; j < counter.length; j++) {
var value = null;
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
await drawAll(arr);
}
}
}
}
return arr;
}Shell Sort
async function shellSort(arr) {
var len = arr.length, gap = 1;
while (gap < len / 5) gap = gap * 5 + 1;
for (; gap > 0; gap = Math.floor(gap / 5)) {
for (var i = gap; i < len; i++) {
var temp = arr[i];
var j;
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
await drawAll(arr);
}
}
return arr;
}Full Source Code
The article provides a complete HTML page containing the canvas element, a start button, and all JavaScript functions shown above, ready to be copied and run.
<canvas id="canvas" width="1000" height="1000" style="background:#000;"></canvas>
<button id="btn">开始排序</button>
// JavaScript code from the sections aboveConclusion
By converting array indices and values into polar coordinates and redrawing the canvas after each sorting step, the article demonstrates a clear method for visualizing the dynamics of many classic sorting algorithms in a web environment.
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.