Introduction
The famous “Spinning Donut” is a mesmerizing animation created using a relatively short piece of C code, known as “donut.c”. In this article, we will go through the process of translating the original “donut.c” code to JavaScript, and make it work in a command-line environment using Node.js. We will also discuss the steps taken to fix the issues encountered during the translation process.
Step 1: Understanding the Original C Code
To translate “donut.c” to JavaScript, we must first understand the underlying logic of the original C code. The key concepts in the code are:
- Trigonometric functions (sine and cosine) are used to create a 3D effect.
- Two nested loops iterate over the points on the surface of the torus (the doughnut shape).
- A z-buffer is employed to determine which points are visible from the viewer’s perspective.
- Characters from a predefined character set are used to represent different levels of brightness.
Step 2: Creating a Basic JavaScript Structure
We will begin by creating a basic JavaScript file named “donut.js”. We will use Node.js to run this script in the command line. The main structure of the JavaScript code should include:
- Importing the required Node.js modules.
- Defining a
renderFrame
function to draw the spinning doughnut for each frame. - Defining a
main
function to handle the animation loop. - Calling the
main
function to start the animation.
Step 3: Translating the C Code to JavaScript
Next, we will carefully translate each part of the C code to its corresponding JavaScript code. The most important parts include:
- Converting the trigonometric functions to their JavaScript equivalents (e.g.,
Math.sin
andMath.cos
). - Adjusting the nested loops and increment values to create a smooth animation.
- Using JavaScript arrays for the z-buffer and output buffer.
- Replacing C’s
putchar
function with thestdout.write
function provided by Node.js.
Step 4: Fixing Aspect Ratio and Positioning Issues
During the translation process, we may encounter issues related to the aspect ratio and positioning of the doughnut. To fix these issues, we can:
- Adjust the coefficients for the x and y calculations in the
renderFrame
function to match the aspect ratio of the characters in the terminal. - Modify the loop increments and x and y calculations to center the doughnut on the screen.
- Test the animation at each step to ensure it renders correctly.
Step 5: Optimizing the JavaScript Code
After fixing the aspect ratio and positioning issues, we might need to optimize the JavaScript code for better performance. We can achieve this by:
- Adjusting the loop increments to find a balance between animation quality and performance.
- Replacing single-letter variable names with more descriptive names for better readability.
Conclusion
In this article, we have walked through the process of translating the original “donut.c” code to JavaScript, running it in a command-line environment using Node.js. By understanding the logic behind the C code, creating a basic JavaScript structure, translating the code, fixing issues, and optimizing performance, we have successfully created a spinning doughnut animation in JavaScript.
Code
Save code as donut.js
Run with:node donut.js
const { stdout } = require('process');
function renderFrame(angleA, angleB) {
const output = Array(1760).fill(' ');
const zBuffer = Array(1760).fill(0);
const sinAngleA = Math.sin(angleA);
const cosAngleA = Math.cos(angleA);
const sinAngleB = Math.sin(angleB);
const cosAngleB = Math.cos(angleB);
for (let loopA = 0; loopA < 2 * Math.PI; loopA += 0.1) {
const cosLoopA = Math.cos(loopA);
const sinLoopA = Math.sin(loopA);
for (let loopB = 0; loopB < 2 * Math.PI; loopB += 0.05) {
const sinLoopB = Math.sin(loopB);
const cosLoopB = Math.cos(loopB);
const h = cosLoopA + 2;
const distance = 1 / (sinLoopB * h * sinAngleA + sinLoopA * cosAngleA + 5);
const t = sinLoopB * h * cosAngleA - sinLoopA * sinAngleA;
const x = Math.floor(40 + 20 * distance * (cosLoopB * h * cosAngleB - t * sinAngleB));
const y = Math.floor(12 + 10 * distance * (cosLoopB * h * sinAngleB + t * cosAngleB));
const outputIndex = x + 80 * y;
const brightnessIndex = Math.floor(8 * ((sinLoopA * sinAngleA - sinLoopB * cosLoopA * cosAngleA) * cosAngleB - sinLoopB * cosLoopA * sinAngleA - sinLoopA * cosAngleA - cosLoopB * cosLoopA * sinAngleB));
if (1760 > outputIndex && outputIndex > 0 && distance > zBuffer[outputIndex]) {
zBuffer[outputIndex] = distance;
output[outputIndex] = '.,-~:;=!*#$@'[brightnessIndex > 0 ? brightnessIndex : 0];
}
}
}
stdout.write('\x1b[H');
for (let k = 0; k < 1760; k++) {
stdout.write(output[k]);
if (k % 80 === 79) stdout.write('\n');
}
}
function main() {
let angleA = 0;
let angleB = 0;
setInterval(() => {
angleA += 0.04;
angleB += 0.08;
renderFrame(angleA, angleB);
}, 16);
}
main();