-
-
Save DanielHabib/cc6d511dbbfa2958acf2e08d8ec7934f to your computer and use it in GitHub Desktop.
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
| #!/usr/bin/env python3 | |
| """ | |
| superhelix.py | |
| 20-second 3D voxel animation of a morphing superhelix with DNA-like structure. | |
| Features dynamic radius oscillation, color gradients, particle trails, and smooth rotations. | |
| The superhelix equation: x = (r + s sin n t) cos t, y = (r + s sin n t) sin t, z = s cos n t | |
| Where r is base radius, s is oscillation amplitude, n controls frequency of bulges. | |
| Run: | |
| pip install spatialstudio numpy | |
| python superhelix.py | |
| Outputs: | |
| superhelix.splv | |
| """ | |
| import math | |
| import numpy as np | |
| from colorsys import hsv_to_rgb | |
| from spatialstudio import splv | |
| # ------------------------------------------------- | |
| GRID = 128 # cubic voxel grid size | |
| FPS = 30 # frames per second | |
| DURATION = 20 # seconds | |
| COUNT = 600 # number of particles along helix | |
| OUTPUT = "../outputs/superhelix.splv" | |
| # ------------------------------------------------- | |
| TOTAL_FRAMES = FPS * DURATION | |
| CENTER = np.array([GRID // 2] * 3, dtype=float) | |
| # Superhelix parameters | |
| BASE_RADIUS = GRID * 0.15 # base radius of helix | |
| OSC_AMPLITUDE = GRID * 0.08 # amplitude of radius oscillation | |
| HARMONIC_FREQ = 4 # frequency of radius oscillation (n parameter) | |
| HELIX_HEIGHT = GRID * 0.6 # total height of helix | |
| TURNS = 3 # number of complete turns along height | |
| # Animation parameters | |
| ROTATION_SPEED = 1.5 # rotations per duration around Y axis | |
| MORPH_CYCLES = 2 # how many times parameters morph during animation | |
| TRAIL_LENGTH = 8 # length of particle trails | |
| def smoothstep(edge0: float, edge1: float, x: float) -> float: | |
| """Smooth interpolation function""" | |
| t = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0))) | |
| return t * t * (3 - 2 * t) | |
| def hsv_bytes(h: float, s: float = 1.0, v: float = 1.0) -> tuple: | |
| """Convert HSV to RGB bytes""" | |
| r, g, b = hsv_to_rgb(h % 1.0, s, v) | |
| return int(r * 255), int(g * 255), int(b * 255) | |
| def superhelix_pos(t_param: float, time_factor: float = 0.0) -> np.ndarray: | |
| """ | |
| Calculate position on superhelix with time-varying parameters | |
| t_param: parameter along helix (0 to 2π * TURNS) | |
| time_factor: global animation time (0 to 1) for morphing effects | |
| """ | |
| # Morph the harmonic frequency over time for visual interest | |
| n = HARMONIC_FREQ + 2 * math.sin(time_factor * MORPH_CYCLES * 2 * math.pi) | |
| # Morph oscillation amplitude | |
| s = OSC_AMPLITUDE * (0.5 + 0.5 * math.cos(time_factor * MORPH_CYCLES * 2 * math.pi)) | |
| # Base radius with gentle variation | |
| r = BASE_RADIUS * (1 + 0.2 * math.sin(time_factor * math.pi)) | |
| # Superhelix equations | |
| radius_at_t = r + s * math.sin(n * t_param) | |
| x = radius_at_t * math.cos(t_param) | |
| y = radius_at_t * math.sin(t_param) | |
| z = s * math.cos(n * t_param) + (t_param / (2 * math.pi * TURNS)) * HELIX_HEIGHT - HELIX_HEIGHT/2 | |
| return np.array([x, y, z]) | |
| def rotate_y(vec: np.ndarray, angle: float) -> np.ndarray: | |
| """Rotate vector around Y axis""" | |
| cos_a, sin_a = math.cos(angle), math.sin(angle) | |
| return np.array([ | |
| vec[0] * cos_a + vec[2] * sin_a, | |
| vec[1], | |
| -vec[0] * sin_a + vec[2] * cos_a | |
| ]) | |
| def rotate_x(vec: np.ndarray, angle: float) -> np.ndarray: | |
| """Rotate vector around X axis""" | |
| cos_a, sin_a = math.cos(angle), math.sin(angle) | |
| return np.array([ | |
| vec[0], | |
| vec[1] * cos_a - vec[2] * sin_a, | |
| vec[1] * sin_a + vec[2] * cos_a | |
| ]) | |
| # Pre-compute parameter values along helix | |
| t_vals = np.linspace(0, 2 * math.pi * TURNS, COUNT, endpoint=False) | |
| # Initialize particle trail history | |
| particle_trails = [[] for _ in range(COUNT)] | |
| enc = splv.Encoder(GRID, GRID, GRID, framerate=FPS, outputPath=OUTPUT) | |
| print(f"Encoding {TOTAL_FRAMES} frames for superhelix animation...") | |
| for frame_idx in range(TOTAL_FRAMES): | |
| global_time = frame_idx / TOTAL_FRAMES # 0 to 1 | |
| # Rotation angles | |
| rot_y = global_time * ROTATION_SPEED * 2 * math.pi | |
| rot_x = math.sin(global_time * math.pi) * 0.3 # gentle nodding motion | |
| frame = splv.Frame(GRID, GRID, GRID) | |
| for i in range(COUNT): | |
| # Calculate position on superhelix | |
| helix_pos = superhelix_pos(t_vals[i], global_time) | |
| # Apply rotations | |
| rotated_pos = rotate_y(helix_pos, rot_y) | |
| rotated_pos = rotate_x(rotated_pos, rot_x) | |
| # Translate to center | |
| world_pos = CENTER + rotated_pos | |
| # Add to trail history | |
| particle_trails[i].append(world_pos.copy()) | |
| if len(particle_trails[i]) > TRAIL_LENGTH: | |
| particle_trails[i].pop(0) | |
| # Render particle trail | |
| for trail_idx, trail_pos in enumerate(particle_trails[i]): | |
| x, y, z = trail_pos.astype(int) | |
| if 0 <= x < GRID and 0 <= y < GRID and 0 <= z < GRID: | |
| # Color based on position along helix and trail age | |
| hue_base = (i / COUNT) * 0.8 # spread across most of hue spectrum | |
| hue_offset = global_time * 0.2 # slowly shift colors over time | |
| hue = (hue_base + hue_offset) % 1.0 | |
| # Trail fading effect | |
| trail_alpha = (trail_idx + 1) / len(particle_trails[i]) | |
| saturation = 0.8 + 0.2 * trail_alpha | |
| brightness = 0.4 + 0.6 * trail_alpha | |
| # Add some sparkle to the newest trail points | |
| if trail_idx == len(particle_trails[i]) - 1: | |
| brightness = min(1.0, brightness + 0.3 * math.sin(global_time * 20 + i * 0.1)) | |
| color = hsv_bytes(hue, saturation, brightness) | |
| frame.set_voxel(x, y, z, color) | |
| # Add extra brightness for main particle (trail head) | |
| if trail_idx == len(particle_trails[i]) - 1: | |
| # Add neighboring voxels for larger particle effect | |
| for dx in [-1, 0, 1]: | |
| for dy in [-1, 0, 1]: | |
| for dz in [-1, 0, 1]: | |
| nx, ny, nz = x + dx, y + dy, z + dz | |
| if (0 <= nx < GRID and 0 <= ny < GRID and 0 <= nz < GRID and | |
| abs(dx) + abs(dy) + abs(dz) <= 1): | |
| bright_color = hsv_bytes(hue, saturation, min(1.0, brightness * 0.7)) | |
| frame.set_voxel(nx, ny, nz, bright_color) | |
| enc.encode(frame) | |
| # Progress indicator | |
| if frame_idx % FPS == 0: | |
| seconds_done = frame_idx // FPS + 1 | |
| print(f" second {seconds_done} / {DURATION}") | |
| enc.finish() | |
| print("Done. Saved", OUTPUT) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment