Translating Donut.c to JavaScript: A Step-by-Step Guide

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.

Donut.c translation to JavaScript

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:

  1. Trigonometric functions (sine and cosine) are used to create a 3D effect.
  2. Two nested loops iterate over the points on the surface of the torus (the doughnut shape).
  3. A z-buffer is employed to determine which points are visible from the viewer’s perspective.
  4. 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:

  1. Importing the required Node.js modules.
  2. Defining a renderFrame function to draw the spinning doughnut for each frame.
  3. Defining a main function to handle the animation loop.
  4. 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:

  1. Converting the trigonometric functions to their JavaScript equivalents (e.g., Math.sin and Math.cos).
  2. Adjusting the nested loops and increment values to create a smooth animation.
  3. Using JavaScript arrays for the z-buffer and output buffer.
  4. Replacing C’s putchar function with the stdout.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:

  1. Adjust the coefficients for the x and y calculations in the renderFrame function to match the aspect ratio of the characters in the terminal.
  2. Modify the loop increments and x and y calculations to center the doughnut on the screen.
  3. 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:

  1. Adjusting the loop increments to find a balance between animation quality and performance.
  2. 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();

Leave a Reply

Your email address will not be published. Required fields are marked *