Skip to content

Instantly share code, notes, and snippets.

@Denbergvanthijs
Last active April 28, 2025 08:50
Show Gist options
  • Save Denbergvanthijs/7f6936ca90a683d37216fd80f5750e9c to your computer and use it in GitHub Desktop.
Save Denbergvanthijs/7f6936ca90a683d37216fd80f5750e9c to your computer and use it in GitHub Desktop.
3D spinning donut in Python. Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
import numpy as np
screen_size = 40
theta_spacing = 0.07
phi_spacing = 0.02
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")
A = 1
B = 1
R1 = 1
R2 = 2
K2 = 5
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))
def render_frame(A: float, B: float) -> np.ndarray:
"""
Returns a frame of the spinning 3D donut.
Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
"""
cos_A = np.cos(A)
sin_A = np.sin(A)
cos_B = np.cos(B)
sin_B = np.sin(B)
output = np.full((screen_size, screen_size), " ") # (40, 40)
zbuffer = np.zeros((screen_size, screen_size)) # (40, 40)
cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing)) # (315,)
sin_phi = np.sin(phi) # (315,)
cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing)) # (90,)
sin_theta = np.sin(theta) # (90,)
circle_x = R2 + R1 * cos_theta # (90,)
circle_y = R1 * sin_theta # (90,)
x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T # (90, 315)
y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T # (90, 315)
z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T # (90, 315)
ooz = np.reciprocal(z) # Calculates 1/z
xp = (screen_size / 2 + K1 * ooz * x).astype(int) # (90, 315)
yp = (screen_size / 2 - K1 * ooz * y).astype(int) # (90, 315)
L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta) # (315, 90)
L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A)) # (315, 90)
L = np.around(((L1 + L2) * 8)).astype(int).T # (90, 315)
mask_L = L >= 0 # (90, 315)
chars = illumination[L] # (90, 315)
for i in range(90):
mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]]) # (315,)
zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])
return output
def pprint(array: np.ndarray) -> None:
"""Pretty print the frame."""
print(*[" ".join(row) for row in array], sep="\n")
if __name__ == "__main__":
for _ in range(screen_size * screen_size):
A += theta_spacing
B += phi_spacing
print("\x1b[H")
pprint(render_frame(A, B))
@AlfonsoXIII
Copy link

Awesome!

@Vanduc006
Copy link

nice!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

@Dang-pig
Copy link

cool!

@Bal4nce123123
Copy link

Really cool!

Putting a sleep of 0.05s at for loop make it perfect.

what line bro?

@RathoreAgastya
Copy link

Umm, I have took this code and copied it to one of my projects
don't worry I gave credit
Just want your permission
btw this is the link to my project
https://github.com/RathoreAgastya/terminal-in-python

@He5am
Copy link

He5am commented Oct 8, 2022

Really cool!
Putting a sleep of 0.05s at for loop make it perfect.

what line bro?

did you understood ?
you can import time
then add this line time.sleep(0.08)
at the top of the line 22

@merikhimo
Copy link

it's not working for me, something about no module named numpy

just download it:
pip install numpy

@avdo317
Copy link

avdo317 commented Feb 3, 2023

My donut won't stay in place and spin, it just creates new frames under itself :( I'm using pycharm

@blue4427
Copy link

blue4427 commented Mar 7, 2023

nice

@Duxedough
Copy link

Is there a way to slow down the donut? Mine is spinning like a BeyBlade.

@iurijw
Copy link

iurijw commented Dec 28, 2023

@Duxedough

Is there a way to slow down the donut? Mine is spinning like a BeyBlade.

For sure! Follow this instructions:

  1. Import sleep function from time module.
import numpy as np
from time import sleep  # Add this module

# Rest of the code [...]
  1. Add sleep function in loop.
# [...] Rest of the code ^

if __name__ == "__main__":
    for _ in range(screen_size * screen_size):
        A += theta_spacing
        B += phi_spacing
        print("\x1b[H")
        pprint(render_frame(A, B))
        sleep(0.05)                # Add sleep funcion here!
                                   # Try diffrent seconds!

Complete "slowed" code :

import numpy as np
from time import sleep  

screen_size = 40
theta_spacing = 0.07
phi_spacing = 0.02
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")

A = 1
B = 1
R1 = 1
R2 = 2
K2 = 5
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))


def render_frame(A: float, B: float) -> np.ndarray:
    """
    Returns a frame of the spinning 3D donut.
    Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
    """
    cos_A = np.cos(A)
    sin_A = np.sin(A)
    cos_B = np.cos(B)
    sin_B = np.sin(B)

    output = np.full((screen_size, screen_size), " ")  # (40, 40)
    zbuffer = np.zeros((screen_size, screen_size))  # (40, 40)

    cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing))  # (315,)
    sin_phi = np.sin(phi)  # (315,)
    cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing))  # (90,)
    sin_theta = np.sin(theta)  # (90,)
    circle_x = R2 + R1 * cos_theta  # (90,)
    circle_y = R1 * sin_theta  # (90,)

    x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T  # (90, 315)
    y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T  # (90, 315)
    z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T  # (90, 315)
    ooz = np.reciprocal(z)  # Calculates 1/z
    xp = (screen_size / 2 + K1 * ooz * x).astype(int)  # (90, 315)
    yp = (screen_size / 2 - K1 * ooz * y).astype(int)  # (90, 315)
    L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta)  # (315, 90)
    L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A))  # (315, 90)
    L = np.around(((L1 + L2) * 8)).astype(int).T  # (90, 315)
    mask_L = L >= 0  # (90, 315)
    chars = illumination[L]  # (90, 315)

    for i in range(90):
        mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]])  # (315,)

        zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
        output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])

    return output


def pprint(array: np.ndarray) -> None:
    """Pretty print the frame."""
    print(*[" ".join(row) for row in array], sep="\n")


if __name__ == "__main__":
    for _ in range(screen_size * screen_size):
        A += theta_spacing
        B += phi_spacing
        print("\x1b[H")
        pprint(render_frame(A, B))
        sleep(0.05)                # Add sleep funcion here!
                                   # Try diffrent seconds!

@Duxedough
Copy link

Thank you very much, iijwpy! I enjoy my donut much better now. Stay blessed!

@EraserGuy
Copy link

miih cant run it

@FaysouRGB
Copy link

Nice

@Kipstal
Copy link

Kipstal commented Jun 5, 2024

im so happy

@longz85
Copy link

longz85 commented Nov 9, 2024

using os library.It better

@longz85
Copy link

longz85 commented Nov 9, 2024

like this:

import numpy as np
from time import sleep
import os

screen_size = 40
theta_spacing = 0.07
phi_spacing = 0.02
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")

A = 1
B = 1
R1 = 1
R2 = 2
K2 = 5
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))

def render_frame(A: float, B: float) -> np.ndarray:
"""
Returns a frame of the spinning 3D donut.
Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
"""
cos_A = np.cos(A)
sin_A = np.sin(A)
cos_B = np.cos(B)
sin_B = np.sin(B)

output = np.full((screen_size, screen_size), " ")  # (40, 40)
zbuffer = np.zeros((screen_size, screen_size))  # (40, 40)

cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing))  # (315,)
sin_phi = np.sin(phi)  # (315,)
cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing))  # (90,)
sin_theta = np.sin(theta)  # (90,)
circle_x = R2 + R1 * cos_theta  # (90,)
circle_y = R1 * sin_theta  # (90,)

x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T  # (90, 315)
y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T  # (90, 315)
z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T  # (90, 315)
ooz = np.reciprocal(z)  # Calculates 1/z
xp = (screen_size / 2 + K1 * ooz * x).astype(int)  # (90, 315)
yp = (screen_size / 2 - K1 * ooz * y).astype(int)  # (90, 315)
L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta)  # (315, 90)
L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A))  # (315, 90)
L = np.around(((L1 + L2) * 8)).astype(int).T  # (90, 315)
mask_L = L >= 0  # (90, 315)
chars = illumination[L]  # (90, 315)

for i in range(90):
    mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]])  # (315,)

    zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
    output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])

return output

def pprint(array: np.ndarray) -> None:
"""Pretty print the frame."""
print(*[" ".join(row) for row in array], sep="\n")

if name == "main":
for _ in range(screen_size * screen_size):
A += theta_spacing
B += phi_spacing
print("\x1b[H")
os.system('cls')
pprint(render_frame(A, B))
sleep(0.05)

@The-Final-Apex
Copy link

My doughnut is not spinning :(. I'm getting snapshots for each timestamp.

probably bc of a computer ocnfiguration smwhere in ur pc ig or just context bro

@thearch-user
Copy link

i use arch btw

@thearch-user
Copy link

pacman<apt, powershell, and yum just saying btw

@thearch-user
Copy link

vim < visual studio code just sayiing btw

@thearch-user
Copy link

vim < visual studio code just sayiing btw

of course you guys
cant even use vim

@thearch-user
Copy link

vim < visual studio code just sayiing btw

of course you guys cant even use vim

to advanced for you fellas

@The-Final-Apex
Copy link

pacman<apt, powershell, and yum just saying btw

bros gonna make me sue all arch users....

@thearch-user
Copy link

yeah we will see about that lil bro
@The-Final-Apex

@The-Final-Apex
Copy link

yeah we will see about that lil bro @The-Final-Apex

Nahhhhh just realized i don thave the power to do so.....

@The-Final-Apex
Copy link

vim < visual studio code just sayiing btw

of course you guys cant even use vim

to advanced for you fellas

also your talking to a notepad coder..... i eat vim for breakfast

@thearch-user
Copy link

lil bro I use hyprland

@thearch-user
Copy link

be stuck with you shitty windows

@The-Final-Apex
Copy link

bruh i hate windows... i use LFS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment