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


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.


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.


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];

  for (let k = 0; k < 1760; 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);