ASCII Donut Math Animation — 30 Days Web Challenge Day 2
The classic donut.c torus rendered in real-time ASCII art with sin/cos math, z-buffer depth, and luminance mapping — with a glitch mode that scrambles into chaos when you click it.
// table of contents (9 sections)
Try it live at 30days.abduarrahman.com and the source code is on GitHub.
Live Demo
The ASCII donut spins on the landing page — click it 5 times to trigger the explosion:
The Origin
Every programmer has seen donut.c — the legendary C code that renders a spinning 3D torus using ASCII characters. It’s a rite of passage.
For Day 2 of the 30 Days Web Challenge, I wanted to bring this classic to the browser — not just as a static render, but as an interactive element with glitch effects and sound. The donut sits on the landing page as the “0” in “30 Days”, and clicking it reveals hidden easter eggs.
What I Built
A real-time ASCII torus renderer with:
- Mathematical torus rendering — the classic sin/cos projection with z-buffer depth sorting and luminance mapping
- Smooth rotation — two rotation angles (A and B) increment each frame for continuous spinning
- Glitch mode — clicking the donut adds random rotation offsets and luminance scrambling
- Neon glow styling — cyan text with
textShadowfor that retro terminal feel - Interactive easter egg — click the donut 5 times on the landing page to trigger an explosion with particles
How It Works
The Torus Rendering Algorithm
The core is the classic donut math — project a 3D torus onto a 2D character grid using two rotation angles:
const render = () => {
const b = new Int8Array(width * height).fill(-1); // output buffer
const z = new Float32Array(width * height); // z-buffer
const sA = Math.sin(A), cA = Math.cos(A);
const sB = Math.sin(B), cB = Math.cos(B);
for (let j = 0; j < 6.283185; j += 0.07) { // theta: around the tube
const st = Math.sin(j), ct = Math.cos(j);
for (let i = 0; i < 6.283185; i += 0.02) { // phi: around the ring
const sp = Math.sin(i), cp = Math.cos(i);
const h = ct + 2;
const D = 1 / (sp * h * sA + st * cA + 5); // perspective
const t = sp * h * cA - st * sA;
const x = ~~(cx + kx * D * (cp * h * cB - t * sB));
const y = ~~(cy + ky * D * (cp * h * sB + t * cB));
const o = x + width * y;
const N = ~~(8 * ((st * sA - sp * ct * cA) * cB
- sp * ct * sA - st * cA - cp * ct * sB));
if (y > 0 && y < height && x > 0 && x < width && D > z[o]) {
z[o] = D;
b[o] = N > 0 ? N : -1;
}
}
}
// Render to <pre> element using luminance characters
if (preRef.current) {
let s = "";
for (let k = 0; k < width * height; k++) {
if (k > 0 && k % width === 0) s += "\n";
s += b[k] >= 0 ? lum[b[k]] : " ";
}
preRef.current.textContent = s;
}
A += 0.015;
B += 0.008;
frameId = requestAnimationFrame(render);
};
The luminance string .,-~:;=!*#$@ maps brightness values to ASCII characters — from dim (dot) to bright (@).
Glitch Mode
When the donut is clicked on the landing page, random offsets are injected into the rotation calculations, and 15% of luminance values get randomly scrambled:
// Glitch: add random rotation offset
const glitchA = glitching ? (Math.random() - 0.5) * 0.5 : 0;
const glitchB = glitching ? (Math.random() - 0.5) * 0.3 : 0;
// During glitch, randomly scramble luminance
if (glitching && Math.random() < 0.15) {
b[o] = ~~(Math.random() * 12);
} else {
b[o] = N > 0 ? N : -1;
}
The visual styling switches from calm cyan to chaotic rainbow with red/orange glow:
style={{
color: glitching ? `hsl(${Math.random() * 360}, 100%, 70%)` : "#00AFFF",
textShadow: glitching
? "0 0 8px #ff0066, 0 0 20px #ff6600"
: "0 0 6px #00AFFF, 0 0 20px #0077ff",
}}
The 5-Click Easter Egg
The donut is interactive — each click triggers a 1-second glitch with a synthesized sound. After 5 clicks, the donut explodes into 30 colored particles:
const handleDonutClick = useCallback(() => {
if (isGlitching || isExploding || showDonutPopup) return;
const newCount = donutClicks + 1;
if (newCount >= 5) {
// BOOM!
setDonutClicks(0);
setIsExploding(true);
playExplosionSound();
const colors = ["#00AFFF", "#00E676", "#ff0066", "#ff6600", "#6C5CE7", "#FFD700"];
const particles = Array.from({ length: 30 }, (_, i) => ({
id: explosionId.current++,
x: 50, y: 40,
angle: (i / 30) * Math.PI * 2 + Math.random() * 0.5,
speed: 5 + Math.random() * 15,
color: colors[i % colors.length],
}));
setExplosionParticles(particles);
} else {
// Glitch for 1 second
setDonutClicks(newCount);
setIsGlitching(true);
playGlitchSound();
}
}, [donutClicks, isGlitching, isExploding, showDonutPopup]);
Tech Stack
| Technology | Purpose |
|---|---|
| Next.js | React framework |
| TypeScript | Type-safe math operations |
| Canvas / pre element | ASCII character rendering |
| Web Audio API | Glitch and explosion sound synthesis |
| Framer Motion | Particle animations for explosion |
Links
- Live Demo: 30days.abduarrahman.com
- Source Code: github.com/ab2rahman/30days-web-challenge
- Key File:
DonutAnimation.tsx - Original donut.c: a1k0n.net/2011/07/20/donut-math.html
Follow the challenge:
- Instagram: @abduarrahman
- YouTube: @abduarrahmanscode
- TikTok: @anduarrahmans
Support the challenge:
- Ko-fi: ko-fi.com/abduarrahman
You might also like
Granloop Arena TV — Pixel Art Fighting Game — 30 Days Web Challenge Day 3
A pixel art 2v2 fighting game running inside a draggable TV overlay. Four characters — two Granmaja vs two Bloop — bounce around an arena, deal random damage on collision, and fight until one team is KO'd.
Magikarp Countdown Challenge — 30 Days Web Challenge Day 1
Roll 4 dice to get a random target number, then try to stop a timer at exactly that many milliseconds. Hit it and watch the countdown drop. Miss it and time goes up. Catch the hidden Magikarp to begin!
Zoro Santoryu Splash Screen — 30 Days Web Challenge Day 4
A cinematic splash screen featuring pixel art Zoro running across the screen, then unleashing Santoryu — three sword slashes that shatter the page with screen shake, debris particles, and music transitions.
More Posts
API Gateway Patterns: The Front Door to Your Microservices
Web Components 2026: Building Framework-Agnostic UI Libraries
Building Autonomous AI Workflows with LangGraph: A Practical Guide
Building Type-Safe APIs with tRPC in 2026: Full-Stack TypeScript Without Schemas
Database Connection Pooling: Patterns for High-Performance Applications
Prompt Caching: Reduce LLM Costs by 90% with Smart Context Management
Enjoyed This Post?
Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.
