Skip to content

Instantly share code, notes, and snippets.

@Denbergvanthijs
Last active April 13, 2024 12:48
Show Gist options
  • Star 47 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • 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))
@selselse
Copy link

selselse commented May 7, 2021

cool

@Future-anonym
Copy link

nice....it works

@slep0v
Copy link

slep0v commented May 13, 2021

Hi, nice work, but there is a part of code you should pay attention to: lines 48..52.
One can safely assume for torus, that if (x, y) [phi1,theta1] = (x,y)[phi2, theta2], then only one of corresponding luminosities will be positive

But you are working with more complex surfaces like Mobius stripe, you may get following:

output[[2,2], [8,8]] = [10, 3] #I've simplified line 52

as you can see, you will get both wrong output and zbuffer values for x=2, y=8

still wonder, if it is still possible to fill values w/o two cycles for phi and theta

@neutrixs
Copy link

nice

@SenZe19
Copy link

SenZe19 commented May 21, 2021

nice

@rudra1337
Copy link

thanks

@alexanderraiden
Copy link

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

@Denbergvanthijs
Copy link
Author

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

Try pip install numpy in your terminal

@sreenidhibulusu
Copy link

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

@Gost-hacker895
Copy link

cool man.....it works

@FatherMonarch
Copy link

nice

@izumichan11
Copy link

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

@MiguelJCoding101
Copy link

It works for me but not the way i wanted IDK why

@iijwpy
Copy link

iijwpy commented Oct 7, 2021

Really cool!

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

@AugustLee25
Copy link

it's fun, but recommend you need to install pip numpy first .

@AugustLee25
Copy link

Uploading 2021-10-14 20-33-50_Trim.mp4…

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

@iijwpy
Copy link

iijwpy 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

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