🌈 Interactive Rainbow Torus Tutorial

This tutorial breaks down how to create a 3D rainbow torus (AKA donut 😉) that rotates based on pointer movement using pure JavaScript and the HTML5 Canvas API.

Click here for fullscreen version

📋 Project Overview

We're building a 3D graphics visualization that involves:

🎨 Part 1: Canvas Setup

HTML Structure

<canvas id="c"></canvas>

We use a simple canvas element. The styling makes it fullscreen with a black background.

JavaScript Setup

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

We get the canvas element and its 2D rendering context, then size it to fill the browser window.

🔄 Part 2: Rotation State

let rotX = 0;
let rotY = 0;
let targetRotX = 0;
let targetRotY = 0;
Why four rotation variables?

🍩 Part 3: Generating the Torus

const R = 150; // Major radius (distance from center to tube center)
const r = 60;  // Minor radius (tube thickness)
const segments = 40; // How many segments around the major circle
const tubes = 20;    // How many segments around the tube

🧮 Torus Mathematics

A torus is created using parametric equations with two angles:

The parametric equations are:

x = (R + r × cos(v)) × cos(u)
y = (R + r × cos(v)) × sin(u)
z = r × sin(v)

generateTorus() Function

function generateTorus() {
    const vertices = [];
    for (let i = 0; i <= segments; i++) {
        const u = (i / segments) * Math.PI * 2;
        for (let j = 0; j <= tubes; j++) {
            const v = (j / tubes) * Math.PI * 2;
            const x = (R + r * Math.cos(v)) * Math.cos(u);
            const y = (R + r * Math.cos(v)) * Math.sin(u);
            const z = r * Math.sin(v);
            const hue = (i / segments) * 360;
            vertices.push({ x, y, z, hue });
        }
    }
    return vertices;
}

How it works:

  1. Loop through segments around the major circle
  2. For each segment, loop around the tube
  3. Calculate x, y, z coordinates using torus equations
  4. Assign a hue value (0-360) based on position for rainbow effect
  5. Store all vertices in an array

🔄 Part 4: 3D Rotations

Rotation Around X-axis

function rotateX(p, angle) {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    return {
        x: p.x,
        y: p.y * cos - p.z * sin,
        z: p.y * sin + p.z * cos,
        hue: p.hue
    };
}

X-axis rotation affects the y and z coordinates. The x coordinate stays the same. This creates a rotation like nodding your head.

Rotation Around Y-axis

function rotateY(p, angle) {
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    return {
        x: p.x * cos + p.z * sin,
        y: p.y,
        z: -p.x * sin + p.z * cos,
        hue: p.hue
    };
}

Y-axis rotation affects the x and z coordinates. The y coordinate stays the same. This creates a rotation like shaking your head.

Rotation Matrices: These functions implement 3D rotation matrices. We pre-calculate cos and sin for efficiency since they're used multiple times.

📐 Part 5: 3D to 2D Projection

function project(p) {
    const scale = 400 / (400 + p.z);
    return {
        x: p.x * scale + canvas.width / 2,
        y: p.y * scale + canvas.height / 2,
        z: p.z,
        hue: p.hue
    };
}

Perspective Projection

This creates a perspective effect where objects farther away (larger z) appear smaller:

🎬 Part 6: Animation Loop

function draw() {
    // Clear canvas
    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // Smooth rotation interpolation
    rotX += (targetRotX - rotX) * 0.1;
    rotY += (targetRotY - rotY) * 0.1;
    
    // Generate and transform vertices
    const vertices = generateTorus();
    const projected = vertices.map(v => {
        let p = rotateX(v, rotX);
        p = rotateY(p, rotY);
        return project(p);
    }).sort((a, b) => a.z - b.z);
    
    // Draw points
    projected.forEach(p => {
        ctx.fillStyle = `hsl(${p.hue}, 100%, 50%)`;
        ctx.beginPath();
        ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
        ctx.fill();
    });
    
    requestAnimationFrame(draw);
}

Step by step:

  1. Clear the canvas: Fill with black
  2. Smooth interpolation: Gradually move current rotation toward target (10% each frame)
  3. Generate vertices: Create all torus points
  4. Transform: Apply rotations and projection to each vertex
  5. Sort by depth: Draw farther points first (painter's algorithm)
  6. Draw: Render each point as a colored circle
  7. Loop: Schedule next frame
Smooth Animation: The formula rotX += (targetRotX - rotX) * 0.1 is called "linear interpolation" or "lerp". It moves 10% of the distance toward the target each frame, creating smooth easing.

🖱️ Part 7: Mouse Interaction

canvas.addEventListener('pointermove', (e) => {
    targetRotY = (e.clientX / canvas.width - 0.5) * Math.PI * 2;
    targetRotX = (e.clientY / canvas.height - 0.5) * Math.PI * 2;
});

How it works:

📱 Part 8: Responsive Design

window.addEventListener('resize', () => {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
});

This ensures the canvas always fills the window when resized.

🎨 Bonus: The Rainbow Effect

const hue = (i / segments) * 360;
ctx.fillStyle = `hsl(${p.hue}, 100%, 50%)`;

HSL Color Space:

🚀 Key Concepts Summary

Mathematical Concepts

Programming Concepts

Graphics Concepts

🔧 Possible Enhancements

💡 Pro Tip: Try changing the torus parameters (R, r, segments, tubes) to see how they affect the shape. Experiment with different projection distances and rotation speeds!
Check out more JS tutorials:

Oldschool fire effect (20 lines)

Fireworks (60 lines)

Animated fractal (32 lines)

Physics engine for beginners

Yin Yang with a twist (4 circles and 20 lines)

Tile map editor (70 lines)

Interactive animated sprites