Show code
<think>
Okay, so I need to create an HTML file that animates the least-significant-digit radix sort using a canvas. The user wants it to be self-contained, no libraries, and under 7000 characters. Let me break this down.
First, the structure. I'll start with the basic HTML setup: doctype, html tags, head with a title, and a body. The canvas element will be in the body. Since it's self-contained, all the JavaScript has to be inside a script tag.
The animation needs to show bars moving as each digit place is processed. The bars are vertical, so I'll represent each with a rectangle. The number of bars is around 80, so maybe 80 elements. They should start with random heights.
Radix sort works by processing each digit from least to most significant. For each digit place (like units, tens, hundreds), we distribute the numbers into buckets based on that digit, then collect them back in order. Since it's LSD, we start with the rightmost digit.
The animation should show the current digit place and the pass number. Each pass corresponds to a digit place. So, for each digit position (starting from 0 for units), we'll need to distribute the bars into buckets based on that digit, then reorder them. The bars should move to their new positions while being colored according to their current bucket.
Wait, how do I handle the animation? Using requestAnimationFrame for smooth animation. The steps would be:
1. Initialize bars with random heights.
2. Start the first pass (digit 0, units place).
3. For each pass, determine the digit place, separate the numbers into buckets, then animate moving the bars into their new positions based on the buckets.
4. After the current digit place is processed (all passes for that digit are done), move to the next digit place (tens, hundreds, etc.).
5. Repeat until all digits have been processed.
6. At the end, restart with new random bars.
Hmm, but how to represent the digit places? Since numbers can be up to the maximum possible height, which is probably up to the canvas height. Wait, in the code, the bars' heights are random, so their maximum possible value would determine how many digits we need. But for simplicity, maybe just process up to 3 or 4 digits, which should cover typical random numbers.
Wait, but to be precise, we need to find the maximum number of digits in the initial array. Let me think: when starting, the heights are random, say between 0 and 500 (the canvas height maybe 500px). So the maximum number of digits would be 3 (since 500 has 3 digits). So the digit places would be 0 (units), 1 (tens), 2 (hundreds). So three passes total.
Wait, but how to handle the digit extraction. For each number, to get the digit at position d (starting from 0 for units), it would be (number / 10^d) % 10.
The animation steps per pass:
- For each bar, calculate its current digit (based on current digit place).
- Assign each bar to a bucket (0-9) based on that digit.
- The order of the bars in each pass should be the order of the buckets. So all the 0 bucket bars come first, then 1, etc.
- The bars need to move to their new positions in the order determined by the bucket sorting.
But how to animate this? Each bar should smoothly move from its current position to its new position in the next step.
Wait, but the animation needs to run in a way that each frame moves the bars closer to their target positions. Each pass (digit place) will have multiple frames where the bars move step by step, rather than jumping.
Alternatively, perhaps during the processing of each digit place, the bars are reordered in their positions based on their digit, and then the bars animate moving into their new order. So the process is:
- For the current digit place, compute the sorted order based on the current digit.
- Then, create an animation where each bar moves from their current position to their new position in the sorted order of the digit.
- After all bars have moved, proceed to the next digit place.
This way, each digit step is animated as a separate phase, with the bars moving into their new order based on that digit.
So the steps in code:
Initialize variables:
- An array of bars (each with a current height and target position).
- Keep track of current digit (starting at 0), pass count (each digit is a pass), and the current animation phase (whether moving to the next position).
The animation loop (requestAnimationFrame) will handle moving the bars towards their target positions. When they reach their targets, proceed to the next phase (next digit).
But how to structure this:
Maybe, the main loop (animation) checks if there is a current target setup. If not, then compute the next target positions for the current digit place.
Wait, here's a possible approach:
- The main animation loop runs continuously (using requestAnimationFrame).
- When in a "processing digit" phase, the bars are moving to their new positions based on the current digit.
- Once all bars have reached their target positions for the current digit, switch to the next digit.
So steps in code:
Initialize bars with random heights and starting positions. Positions are evenly spaced along the x-axis, so each bar has an initial x-coordinate based on its index.
The process for each digit:
1. Determine the current digit (starting at 0, then incrementing).
2. For each bar, compute the digit at the current digit position.
3. Group the bars by their digit (0-9).
4. The new positions of the bars are determined by the order of the groups and their order within the groups.
5. Assign each bar's target x-coordinate (based on their new position in the sorted order).
6. Then, in the animation loop, each frame moves the bars towards their target x-coordinate by a certain amount.
7. Once all bars have moved, move to the next digit.
Wait, but the target x-coordinate needs to be calculated such that the bars are ordered according to their digit's bucket. For instance, first all the 0 digit bars, then 1, etc., and within each digit group, their order is the same as their previous order before the current digit's processing.
Wait, the order in each bucket should be the same as they were when they came from the previous pass. So, for example, after processing the units digit, when moving to the tens digit, the current order is based on units. Now, in the next pass (tens), the buckets are based on tens digit, so each bucket (0-9) will hold the bars with that tens digit, ordered as they were before (i.e., their previous order from the units pass). This way, when you collect the buckets in order, the numbers are sorted by both tens and units digits.
Therefore, the target positions for the bars in the current digit's pass are arranged by:
- For each digit (d) from 0 to 9:
- Collect all bars where the current digit is d, and their current order (from the previous passes)
- Concatenate all these lists to get the new order.
The new x-positions are based on their index in this new order.
So for each bar in the new order list, the target x is based on their new index (position in the list).
Therefore, each bar will have a target x that corresponds to their new position in the sorted buckets.
Now, the animation needs to move each bar's x-coordinate from its current position to this target x over several frames.
To track this, each bar could have a current x and a target x, and during animation frames, we move toward the target by some delta (like 10% per frame or something).
This way, each bar is smoothly moving to its new position.
Now, the steps in code:
First, setup:
- Create an array of bars, each with:
- x: current x position
- target_x: target x position (initialized to x)
- height: the current height (starts as random)
- digit: current digit value at the current digit place (but maybe not needed)
- color: color based on the current digit's bucket (but only when in a pass, else default color?)
Wait, colors: during the current digit's processing, each bar's color should reflect which bucket (digit) they're in. So, when the animation is moving them to their new positions, their color should be based on the current digit's value. So, for each bar, during the current digit's pass, the digit is calculated, and that's used to determine the color.
Hmm, perhaps the color is computed as the current digit's value (from the current digit place). So, during each digit's processing phase, each bar's digit for that place is (bar's height / (10^digit)) % 10. The color can be a function of this digit, maybe a rainbow or sequential colors for digits 0-9.
The HTML and setup:
The canvas should have a reasonable size, say 800x500.
In the JavaScript:
Initialize:
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var bars = [];
var digit = 0;
var pass = 0;
var animating = false;
var step = 0;
var done = false;
Wait, but variables need to be declared properly. Maybe using let and const.
Wait, perhaps better to use an object to hold state variables.
Wait, but to keep it concise, variables should be kept minimal.
First, in the setup:
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var width = canvas.width = 800;
var height = canvas.height = 500;
var numBars = 80;
var bars = Array.from({length: numBars}, (_, i) => ({
x: i * (width / numBars),
targetX: i * (width / numBars),
height: Math.random() * (height - 50) + 20, // between 20 and 450
color: 'white' // default color
}));
var currentDigit = 0;
var currentPass = 0;
var maxDigit = 0;
var isAnimating = false;
var complete = false;
Wait, but how to determine maxDigit? The maximum digit needed can be computed based on the maximum height. So initially, find the max height, then compute how many digits it has. For example, if max is 500, then 3 digits, so maxDigit is 2 (since starting from 0).
Alternatively, compute maxDigit as Math.floor(Math.log10(maxHeight)) or similar. But since we can't know the max in advance, maybe compute it once at the start.
So:
var max = Math.max(...bars.map(b => b.height));
var maxDigit = max === 0 ? 0 : Math.floor(Math.log10(max));
var totalDigits = maxDigit + 1;
That way, totalDigits is the number of digits we need to process.
Wait, but since heights are numbers, the maximum digits needed is the number of digits in the largest number. For example if the maximum height is 999, that's 3 digits, so we need to process digit 0 (units), 1 (tens), 2 (hundreds).
So, totalDigits is that value.
Now, the animation steps:
The main loop is controlled by requestAnimationFrame. Each frame:
If the animation is currently moving towards targets (isAnimating), then we move each bar's x towards targetX by some fraction. Maybe 0.1 per frame? If they reach target, then we can mark that the animation is done.
Once all bars are at their target positions, we can proceed to the next digit.
Wait, but how to know when all bars have arrived. Maybe track a counter of bars not yet arrived.
Alternatively, after a certain number of steps, but that might not be accurate.
Alternatively, in each frame, check if any bar hasn't reached its target yet. If all have, then trigger the next phase.
Hmm, but that could be computationally intensive.
Alternatively, after moving, the targetX can be set to null once reached, and check if all have done so.
Alternatively, for simplicity, just move towards the target by a fixed amount per frame. Once the target is within a small delta, consider it there.
Alternatively, just use an easing function. But to keep it simple, perhaps:
bars.forEach(bar => {
var dx = bar.targetX - bar.x;
bar.x += dx * 0.2; // 20% of the distance per frame
if (Math.abs(dx) < 0.1) bar.x = bar.targetX; // snap when close enough
});
This way, bars move towards their target, and when close enough, snap to it.
So, in the animation loop, after moving, check if any bars are still moving. If none, then proceed to the next phase.
The animation loop function:
function animate() {
if (complete) return;
ctx.clearRect(0, 0, width, height);
// Draw bars and text
bars.forEach(bar => {
ctx.fillStyle = bar.color;
ctx.fillRect(bar.x, height - bar.height, width/numBars, bar.height);
});
// Update positions
if (isAnimating) {
// Move towards target
let allStopped = true;
bars.forEach(bar => {
let dx = bar.targetX - bar.x;
bar.x += dx * 0.2;
if (Math.abs(dx) > 0.1) allStopped = false;
if (Math.abs(dx) < 0.1) bar.x = bar.targetX;
});
if (!allStopped) {
requestAnimationFrame(animate);
return;
} else {
// Animation done, proceed to next digit
isAnimating = false;
processNextDigit();
requestAnimationFrame(animate);
return;
}
} else {
// Not animating, so check if need to do next phase
if (currentDigit < totalDigits) {
// Compute next digit's targets
computeTargetPositions();
isAnimating = true;
requestAnimationFrame(animate);
return;
} else {
// All done, reset
complete = true;
setTimeout(() => {
reset();
complete = false;
animate();
}, 1000); // Wait a sec before restarting
return;
}
}
requestAnimationFrame(animate);
}
Wait, but the logic might need to be adjusted. Let me think again.
The animate function is called every frame.
First, clear the canvas and draw the bars.
Then, if isAnimating, move the bars towards targetX. If all have reached their targets (allStopped is true), then after that frame, we set isAnimating to false and call processNextDigit, which would compute the next step.
Wait, but in the next frame, since isAnimating is false, it would proceed.
Alternatively, after setting allStopped, once it's done, then the processNextDigit is called.
Hmm, perhaps better to structure it as:
Inside the animate function:
if (isAnimating) {
// Move bars
bars.forEach(...);
check if all stopped
if not, continue
else:
isAnimating = false
processNextDigit()
} else {
// not animating
if currentDigit is not done:
compute targets for next digit, set isAnimating to true
then, next frame will start moving
}
Wait, maybe I should restructure the flow.
The phases:
1. Initial state: bars are in original positions, no animation.
When currentDigit < totalDigits:
- Need to process the next digit.
- First, compute the target positions based on current digit.
- Set each bar's targetX to their new position in the sorted buckets.
- Then, start the animation (set isAnimating = true)
- Then, in the next frames, move towards targetX.
Once the animation is done (all stopped), processNextDigit() increments the currentDigit and starts again.
Wait, processNextDigit would:
currentDigit++;
if (currentDigit <= totalDigits) {
computeTargetPositions();
isAnimating = true;
} else {
// done, reset
reset();
}
Wait, but let's think step by step.
First, after initialization:
currentDigit is 0, isAnimating is false.
In the first call to animate:
since isAnimating is false and currentDigit < totalDigits, it calls computeTargetPositions (for digit 0), then sets isAnimating = true.
Then, in the next frame, isAnimating is true, so bars start moving.
Once all bars have reached their targets (after moving for some frames), in the next frame after all are stopped:
- isAnimating becomes false, then processNextDigit is called.
- processNextDigit increments currentDigit to 1, then calls computeTargetPositions again, and sets isAnimating = true again.
- So the next digit's animation starts.
Wait, but processNextDigit should be called after the animation is done. So, the 'processNextDigit' function would handle that.
Wait, let me structure the functions:
The computeTargetPositions() function would:
Given the current digit, re-calculate the targetX positions for all bars based on their digit in that place.
So:
function computeTargetPositions() {
const d = currentDigit;
const divisor = Math.pow(10, d);
const digitBuckets = Array.from({length: 10}, () => []);
// Assign each bar to their bucket based on current digit
bars.forEach(bar => {
const digit = Math.floor(bar.height / divisor) % 10;
bar.digit = digit;
digitBuckets[digit].push(bar);
});
// Flatten the buckets in order to get new order
let newOrder = [];
digitBuckets.forEach(bucket => newOrder = newOrder.concat(bucket));
// Assign targetX based on their new position in newOrder
const barWidth = width / numBars;
newOrder.forEach((bar, index) => {
bar.targetX = index * barWidth;
});
}
Then, after this, the bars have targetX set. Then, the animation starts.
Once all bars have reached their targetX, next digit is processed.
So in the animate function:
if (isAnimating) {
// move, check if stopped
if (allStopped) {
isAnimating = false;
processNextDigit();
}
} else {
// not animating, so need to check if we need to compute next targets
if (currentDigit < totalDigits) {
computeTargetPositions();
isAnimating = true;
} else {
// done, reset
// ... reset code ...
}
}
Wait, but in the animate function, after checking isAnimating, if it's false, but currentDigit < totalDigits, then computeTargetPositions, and set isAnimating to true, then on next frame, the animation starts.
So the processNextDigit() function is actually just the increment of currentDigit and possibly the computeTargetPositions?
Wait, perhaps processNextDigit() would be:
function processNextDigit() {
currentDigit++;
if (currentDigit <= totalDigits) {
// compute next digit's targets
computeTargetPositions();
isAnimating = true;
} else {
// done, reset
setTimeout(reset, 1000);
complete = true;
}
}
Hmm, but the structure in the animate function would need to call processNextDigit after the animation stops.
Alternatively, perhaps the animate function's flow is:
Inside the animate function:
if (isAnimating) {
move bars, check if all stopped.
if stopped, set isAnimating to false, and call processNextDigit()?
}
Wait, perhaps better to structure it as:
Inside the animate function, after moving and checking allStopped:
if (allStopped) {
isAnimating = false;
if (currentDigit < totalDigits) {
currentDigit++;
computeTargetPositions(); // no, wait, currentDigit is now next, so compute with that?
isAnimating = true;
} else {
// done
// reset
}
}
Wait, this is getting a bit tangled. Maybe it's better to structure the processNextDigit as a separate function.
Alternatively, here's a possible structure:
After moving the bars, if all stopped:
- isAnimating = false
- increment currentDigit and check if it's within totalDigits. If yes, then need to start next digit's animation.
Wait, perhaps:
if (allStopped) {
isAnimating = false;
if (currentDigit < totalDigits) {
// move to next digit
currentDigit++;
computeTargetPositions();
isAnimating = true;
} else {
// done, reset
complete = true;
// after a delay, reset
// ...
}
}
But then, in this case, computeTargetPositions() would compute for the new currentDigit.
Wait, that's correct. Because after incrementing currentDigit, computeTargetPositions() will use that new digit.
Yes, that makes sense.
So in the animate function, when allStopped:
We set isAnimating to false, then:
if (currentDigit (now incremented) is still less than totalDigits, we compute the new currentDigit's target positions, and set isAnimating to true again.
Thus, the next frame will start moving towards those targets.
Okay, that seems better.
Now, the color of the bars can be based on their current digit. So during computeTargetPositions, we can set bar.digit, and then in the drawing phase, set color based on that.
In the drawing phase:
bars.forEach(bar => {
ctx.fillStyle = bar.color;
// ... draw
});
But the color depends on bar.digit. So perhaps:
bar.color = `hsl(${bar.digit * 36}, 100%, 50%)`; // or another function.
Wait, in the computeTargetPositions function, after computing the digit for each bar, set the color:
so in computeTargetPositions:
digitBuckets.forEach( (bucket, digit) => {
bucket.forEach(bar => bar.color = `hsl(${digit * 36}, 100%, 50%)`);
});
Alternatively, in computeTargetPositions function, when calculating the digit for each bar:
bar.digit = digit;
bar.color = getColor(digit);
where getColor is a function that returns a color based on digit.
This would color each digit a different color, making it clear which digit is being sorted.
Alternatively, a simple gradient or rainbow.
Alternatively, during the computeTargetPositions, set the color to a digit-specific color.
Now, when the animation is done, and all digits are processed, the bars are sorted by their height? Wait no, because the process is grouping digits, but not sorting. Wait, this is actually a Radix sort visualization.
Radix sort is a non-comparative integer sorting algorithm that sorts data with integer keys by grouping keys by the individual digits which share the same significant position and value.
So the digit being processed is the current digit (units, tens, hundreds etc), and the algorithm groups the numbers into buckets based on that digit, then outputs them in order. After all digits are processed, the numbers are sorted.
Therefore, the way the target positions are computed above is correct for radix sort.
Therefore, once all digits have been processed, the bars will be ordered by their height in the correct sorted order.
Therefore, the reset should randomize the bars again, so that when restarted, it's a new set.
The reset function would re-initialize the bars array with new random heights, reset the currentDigit to 0, and set complete to false.
function reset() {
bars = Array.from({length: numBars}, (_, i) => ({
x: i * (width / numBars),
targetX: i * (width / numBars),
height: Math.random() * (height - 50) + 20,
color: 'white',
digit: 0
}));
max = Math.max(...bars.map(b => b.height));
maxDigit = max === 0 ? 0 : Math.floor(Math.log10(max));
totalDigits = maxDigit + 1;
currentDigit = 0;
currentPass = 0;
isAnimating = false;
complete = false;
}
Now, putting all together.
Also, in the animate function, need to display the current digit and pass.
So add text to the canvas.
In the drawing phase:
Draw the bars.
Then draw text like:
ctx.font = '20px Arial';
ctx.fillStyle = 'black';
ctx.fillText(`Digit: ${currentDigit}`, 10, 30);
ctx.fillText(`Pass: ${currentPass}`, 10, 60);
Wait, but the pass is currentPass, but in this case, each digit is a pass. Hmm, perhaps not necessary. Alternatively, just show the current digit.
Also, when resetting, we need to wait a second before restarting, so the animation can display the final state before resetting.
In the else part of the animate function (when done):
if (currentDigit < totalDigits) {
// ... same as before ...
} else {
complete = true;
setTimeout(() => {
reset();
complete = false;
animate();
}, 1000);
}
Wait, in the else clause (after currentDigit exceeds totalDigits), we set complete = true, then after 1 sec, reset and restart.
Therefore, the animate function continues to be called, but once complete is true, it triggers the reset.
Now, putting all this together, the JavaScript code would be structured as follows:
Initialize variables.
Then define functions computeTargetPositions(), processNextDigit(), reset().
Then the animate function.
Finally, call animate() once to start.
Also, the canvas and context are initialized via querySelector.
Putting this all into code:
But I need to be careful with variable declarations and function order.
Let me try to write the full code step by step.
First, the HTML:
<canvas id="c"></canvas>
The JavaScript:
const canvas = document.querySelector('#c');
const ctx = canvas.getContext('2d');
const width = canvas.width = 800;
const height = canvas.height = 500;
const numBars = 80;
let bars = [];
let currentDigit = 0;
let totalDigits = 0;
let isAnimating = false;
let complete = false;
function reset() {
bars = Array.from({length: numBars}, (_, i) => ({
x: i * (width / numBars),
targetX: i * (width / numBars),
height: Math.random() * (height - 50) + 20,
color: 'white',
digit: 0
}));
const max = Math.max(...bars.map(b => b.height));
totalDigits = max === 0 ? 0 : Math.floor(Math.log10(max)) + 1; // +1 because e.g log10(100)=2, but 3 digits
}
reset();
function computeTargetPositions() {
const d = currentDigit;
const divisor = Math.pow(10, d);
const digitBuckets = Array.from({length: 10}, () => []);
bars.forEach(bar => {
const digit = Math.floor(bar.height / divisor) % 10;
bar.digit = digit;
digitBuckets[digit].push(bar);
});
// Assign colors based on digit
digitBuckets.forEach((bucket, digit) => {
bucket.forEach(bar => bar.color = `hsl(${digit * 36}, 100%, 50%)`);
});
// Flatten the buckets to get new order
const newOrder = digitBuckets.reduce((acc, bucket) => acc.concat(bucket), []);
// Assign target positions
const barWidth = width / numBars;
newOrder.forEach((bar, index) => {
bar.targetX = index * barWidth;
});
}
function animate() {
requestAnimationFrame(animate);
if (complete) return;
ctx.clearRect(0, 0, width, height);
// Draw bars
bars.forEach(bar => {
ctx.fillStyle = bar.color;
ctx.fillRect(bar.x, height - bar.height, width/numBars, bar.height);
});
// Draw info
ctx.fillStyle = 'black';
ctx.font = '20px Arial';
ctx.fillText(`Current Digit: ${currentDigit}`, 10, 30);
// Update positions
if (isAnimating) {
let allStopped = true;
bars.forEach(bar => {
if (bar.x !== bar.targetX) {
const dx = bar.targetX - bar.x;
bar.x += dx * 0.1; // smooth movement
allStopped = false;
}
});
if (allStopped) {
isAnimating = false;
if (currentDigit < totalDigits) {
currentDigit++;
computeTargetPositions();
isAnimating = true;
} else {
complete = true;
setTimeout(() => {
reset();
currentDigit = 0;
computeTargetPositions();
isAnimating = true;
complete = false;
}, 1000);
}
}
} else {
if (currentDigit < totalDigits) {
computeTargetPositions();
isAnimating = true;
}
}
}
// Start the animation
animate();
Wait, but there might be some issues here.
First, in computeTargetPositions, the newOrder is the flattened array, but the targetX for each bar is set to index * barWidth. So the first bucket (digit 0) will be placed first, then digit 1, etc.
This correctly assigns targetX positions according to the bucket order.
In the animate function, during isAnimating, the bars move towards their targetX, using a smooth step (dx * 0.1). So they slowly move to their target.
When allStopped becomes true, meaning all bars have reached their targetX (within some tolerance, but since we're moving by a fraction each frame, it might take a few frames to stop), then we check if currentDigit is still less than totalDigits.
If yes, increment currentDigit, compute the next target positions, and start animating again.
If all digits are processed (currentDigit >= totalDigits), then we set complete to true. After a second, reset the bars and restart.
However, in the code above, after resetting, it calls computeTargetPositions again, which recomputes the first targets for currentDigit=0. Then sets isAnimating to true, so it can start the next cycle.
Wait, in the code:
Inside the else block (currentDigit >= totalDigits):
setTimeout(() => {
reset();
currentDigit = 0;
computeTargetPositions();
isAnimating = true;
complete = false;
}, 1000);
This should restart the animation.
But perhaps need to call computeTargetPositions after reset, because during reset, the bars are initialized to their initial positions (x = i * barWidth, targetX the same, so no movement). So after reset, to start the process again, we need to compute the first targets when currentDigit is 0.
Yes, that seems correct.
Testing this code might reveal some issues, but this is the general approach.
Possible adjustments:
- The color assignment. The current code sets the color based on the digit, but since during the animation, the digit is fixed for each bar (they retain their digit for the current pass), the color remains the digit color throughout. That's okay.
- The movement speed. The current code uses a step of 0.1 of the remaining distance each frame. This could be adjusted for smoother or faster movement.
- The initial reset: when first starting, the bars are placed at their initial positions (x = index * barWidth), but after reset, they are again set to those positions. So the first time computeTargetPositions is called (when currentDigit is 0), it will compute their target positions based on the first digit, which may differ.
Overall, this should work as a visualization of the Radix sort algorithm.