Skip to content

Instantly share code, notes, and snippets.

@Denbergvanthijs
Last active November 9, 2024 08:09
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))
@mitenai
Copy link

mitenai commented Oct 19, 2021

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

if youre using VS code , then you have to put the terminal on fullscreen when you run the code. (click the red circled button in the pic attached). And it runs smoothly Screenshot (6)

Screenshot (5)_LI

for me terminal dosent show up anything after running it yk how to fix?

@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

@Mech-08
Copy link

Mech-08 commented Apr 24, 2024

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

if youre using VS code , then you have to put the terminal on fullscreen when you run the code. (click the red circled button in the pic attached). And it runs smoothly Screenshot (6)
Screenshot (5)_LI

for me terminal dosent show up anything after running it yk how to fix?

Screenshot 2024-04-25 001301
Tap on the blue circled button then you will see the terminal

@FaysouRGB
Copy link

Nice

@Mech-08
Copy link

Mech-08 commented Apr 28, 2024

Nice

thnx :)

@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)

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