Created
February 11, 2026 12:37
-
-
Save JGalego/83d343bb3079b16e7e39553369d1a0e0 to your computer and use it in GitHub Desktop.
Python port of the jellyfish πͺΌ animation from the Wolfram Language snippet πΊ>>>π
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /// script | |
| # requires-python = ">=3.10" | |
| # dependencies = [ | |
| # "matplotlib", | |
| # "numpy", | |
| # "pillow", # for GIF saving | |
| # "ffmpeg" # for MP4 saving; ensure ffmpeg is installed and | |
| # ] | |
| # /// | |
| """ | |
| Python port of the jellyfish animation from the Wolfram Language snippet | |
| https://community.wolfram.com/groups/-/m/t/3637839 | |
| """ | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from matplotlib.animation import FuncAnimation | |
| # ---------- Parameters ---------- | |
| N = 10_000 # number of points (i from 0..9999) | |
| W, H = 400, 400 # original canvas was square 400 x 400 | |
| BG = 9/255.0 # near-black background ~0.035 gray in WL snippet | |
| POINT_SIZE = 0.2 # pixel size; tweak if needed | |
| # Time sweep similar to Manipulate[t, t0 .. t0 + 8Ο]; you can shorten for speed | |
| START = 19.35 # starting phase | |
| END = START + 8*np.pi # end phase | |
| FPS = 30 | |
| SECONDS = 6 # total duration | |
| FRAMES = int(FPS * SECONDS) | |
| def exact_point_data(t): | |
| """ | |
| Compute the x and y coordinates of all N points for a given time t, following | |
| the mathematical formulation from the original Wolfram Language snippet. | |
| """ | |
| i = np.arange(N, dtype=np.float32) | |
| y = i / 790.0 | |
| e = y / 3.0 - 13.0 | |
| k_base = np.where(y < 8.0, | |
| 9.0 + 6.0*np.sin(y**9), | |
| 4.0 + np.cos(y)) | |
| k = k_base * np.cos(i + t/4.0) | |
| d = np.sqrt(k*k + e*e) + np.cos(e + 2.0*t + (i % 2)*4.0) | |
| q = (y * k / 5.0) * (2.0 + np.sin(2.0*d + y - 4.0*t)) + 80.0 | |
| c = d/4.0 - t/2.0 + (i % 2)*3.0 | |
| x = q*np.cos(c) + 200.0 | |
| y_screen = -(q*np.sin(c) + 9.0*d + 60.0) | |
| return x.astype(np.float32), y_screen.astype(np.float32) | |
| # ---------- Matplotlib setup ---------- | |
| plt.style.use("default") | |
| fig = plt.figure(figsize=(W/100, H/100), dpi=100) | |
| ax = plt.axes((0, 0, 1, 1)) | |
| ax.set_facecolor((BG, BG, BG)) | |
| ax.set_xlim(40, 360) | |
| ax.set_ylim(-390, -10) | |
| ax.set_axis_off() | |
| # Initial scatter (empty; weβll set offsets every frame) | |
| # Using PathCollection is fastest; marker size is in points^2; tune for density | |
| scat = ax.scatter([], [], s=POINT_SIZE, c="white", edgecolors="none") | |
| # ---------- Animation loop ---------- | |
| ts = np.linspace(START, END, FRAMES).astype(np.float32) | |
| def init(): | |
| """Initialize the scatter with empty data.""" | |
| scat.set_offsets(np.empty((0, 2), dtype=np.float32)) | |
| return (scat,) | |
| def update(frame_idx): | |
| """Update the scatter offsets for the current frame index.""" | |
| t = ts[frame_idx] | |
| x, y = exact_point_data(t) | |
| # Stack into Nx2 for set_offsets | |
| xy = np.column_stack((x, y)) | |
| scat.set_offsets(xy) | |
| return (scat,) | |
| anim = FuncAnimation( | |
| fig, | |
| update, | |
| frames=FRAMES, | |
| init_func=init, | |
| interval=1000/FPS, | |
| blit=True | |
| ) | |
| if __name__ == "__main__": | |
| # Save GIF (requires pillow) | |
| anim.save( | |
| "jellyfish.gif", | |
| dpi=100, | |
| writer="pillow", | |
| savefig_kwargs={ | |
| 'facecolor': (BG, BG, BG) | |
| } | |
| ) | |
| # Save MP4 (requires ffmpeg) | |
| anim.save( | |
| "jellyfish.mp4", | |
| dpi=100, | |
| fps=FPS, | |
| writer="ffmpeg", | |
| savefig_kwargs={ | |
| 'facecolor': (BG, BG, BG) | |
| } | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment