Skip to content

Instantly share code, notes, and snippets.

@DanielHabib
Created August 26, 2025 12:38
Show Gist options
  • Select an option

  • Save DanielHabib/3a42e691dfcb75ec0bece9e4dbafc65c to your computer and use it in GitHub Desktop.

Select an option

Save DanielHabib/3a42e691dfcb75ec0bece9e4dbafc65c to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
crystalline_spiral.py
18-second 3D voxel animation of a crystalline spiral with hard angular corners.
Features explosive particle formation, sharp geometric transitions, and collapse into singularity.
The crystalline spiral uses sharp angular functions to create hard corners and faceted segments.
Beginning: Particles explode from chaos into formation
Middle: Sharp crystalline spiral rotates with angular segments
End: Collapse into central singularity with energy flash
Run:
pip install spatialstudio numpy
python crystalline_spiral.py
Outputs:
crystalline_spiral.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 = 18 # seconds
COUNT = 1000 # number of particles
OUTPUT = "../outputs/crystalline_spiral.splv"
# -------------------------------------------------
TOTAL_FRAMES = FPS * DURATION
CENTER = np.array([GRID // 2] * 3, dtype=float)
# Crystalline spiral parameters
BASE_RADIUS = GRID * 0.18 # base radius
HEIGHT_RANGE = GRID * 0.4 # total height range
SEGMENTS = 8 # number of angular segments per turn
TURNS = 3 # number of complete turns
CORNER_SHARPNESS = 0.15 # controls how sharp the corners are (lower = sharper)
# Animation phases
EXPLOSION_PHASE = 0.15 # first 15% - explosion into formation
SPIRAL_PHASE = 0.70 # middle 70% - crystalline spiral
COLLAPSE_PHASE = 0.15 # final 15% - collapse to singularity
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 sharpstep(x: float, sharpness: float = 0.1) -> float:
"""Create sharp transitions - lower sharpness = sharper corners"""
return 1.0 / (1.0 + math.exp(-(x - 0.5) / sharpness))
def hsv_bytes(h: float, s: float = 1.0, v: float = 1.0) -> tuple:
"""Convert HSV to RGB bytes"""
# Ensure all values are in proper ranges
h = max(0.0, min(1.0, h % 1.0))
s = max(0.0, min(1.0, s))
v = max(0.0, min(1.0, v))
r, g, b = hsv_to_rgb(h, s, v)
return int(r * 255), int(g * 255), int(b * 255)
def crystalline_spiral_pos(t_param: float, time_factor: float = 0.0) -> np.ndarray:
"""
Calculate position on crystalline spiral with smooth flowing curves and dynamic elements
t_param: parameter along spiral (0 to 2π * TURNS)
time_factor: global animation time (0 to 1) for morphing effects
"""
# Smooth base spiral with dynamic parameters
base_angle = t_param
# Multi-layered radius variations for visual interest
# Primary spiral with breathing motion
radius_base = BASE_RADIUS * (1 + 0.3 * math.sin(time_factor * 3 * math.pi))
# Secondary harmonic for interesting bulges
harmonic_freq = 2.5 + math.sin(time_factor * 2 * math.pi) # varying frequency
radius_harmonic = 0.4 * BASE_RADIUS * math.sin(harmonic_freq * t_param)
# Tertiary modulation for fine detail
detail_freq = 8 + 3 * math.cos(time_factor * 1.5 * math.pi)
radius_detail = 0.15 * BASE_RADIUS * math.sin(detail_freq * t_param + time_factor * 4 * math.pi)
# Combine radius components
final_radius = radius_base + radius_harmonic + radius_detail
# Smooth position calculation with flowing motion
x = final_radius * math.cos(base_angle)
z = final_radius * math.sin(base_angle)
# Smooth Y position with gentle undulation
y_progress = (t_param / (2 * math.pi * TURNS))
base_y = y_progress * HEIGHT_RANGE - HEIGHT_RANGE/2
# Add flowing vertical waves
y_wave1 = 0.1 * HEIGHT_RANGE * math.sin(3 * t_param + time_factor * 2 * math.pi)
y_wave2 = 0.05 * HEIGHT_RANGE * math.sin(7 * t_param - time_factor * 3 * math.pi)
y = base_y + y_wave1 + y_wave2
# Add crystalline segments only at specific intervals for hard corners
segment_angle = 2 * math.pi / SEGMENTS
current_segment = math.floor((t_param % (2 * math.pi)) / segment_angle)
segment_progress = ((t_param % (2 * math.pi)) % segment_angle) / segment_angle
# Apply sharp angular corrections only near segment boundaries
if segment_progress < 0.1 or segment_progress > 0.9:
# Sharp angular transitions for crystalline edges
sharp_progress = sharpstep(segment_progress, CORNER_SHARPNESS)
sharp_angle = current_segment * segment_angle + sharp_progress * segment_angle
# Blend between smooth and sharp
blend_factor = 1.0 - abs(segment_progress - 0.5) * 2 # peaks at boundaries
blend_factor = blend_factor * blend_factor # sharper transition
sharp_x = final_radius * math.cos(sharp_angle)
sharp_z = final_radius * math.sin(sharp_angle)
x = x * (1 - blend_factor) + sharp_x * blend_factor
z = z * (1 - blend_factor) + sharp_z * blend_factor
# Enhanced crystalline texture with flowing patterns
crystal_flow = 0.03 * BASE_RADIUS * (
math.sin(t_param * 11 + time_factor * 5 * math.pi) +
math.cos(t_param * 13 - time_factor * 3 * math.pi) +
math.sin(t_param * 17 + time_factor * 7 * math.pi)
)
# Add swirling motion to the crystal texture
swirl_angle = time_factor * 2 * math.pi + t_param * 0.5
swirl_x = crystal_flow * math.cos(swirl_angle)
swirl_z = crystal_flow * math.sin(swirl_angle)
return np.array([x + swirl_x, y, z + swirl_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
])
def rotate_z(vec: np.ndarray, angle: float) -> np.ndarray:
"""Rotate vector around Z axis"""
cos_a, sin_a = math.cos(angle), math.sin(angle)
return np.array([
vec[0] * cos_a - vec[1] * sin_a,
vec[0] * sin_a + vec[1] * cos_a,
vec[2]
])
# Pre-compute parameter values along spiral
t_vals = np.linspace(0, 2 * math.pi * TURNS, COUNT, endpoint=False)
# Initialize random positions for explosion phase
np.random.seed(42)
explosion_positions = np.random.rand(COUNT, 3) * GRID
explosion_velocities = (np.random.rand(COUNT, 3) - 0.5) * 2.0
enc = splv.Encoder(GRID, GRID, GRID, framerate=FPS, outputPath=OUTPUT)
print(f"Encoding {TOTAL_FRAMES} frames for crystalline spiral animation...")
for frame_idx in range(TOTAL_FRAMES):
global_time = frame_idx / TOTAL_FRAMES # 0 to 1
# Determine animation phase
if global_time < EXPLOSION_PHASE:
# Explosion phase - particles form from chaos
phase_progress = global_time / EXPLOSION_PHASE
formation_factor = smoothstep(0.3, 1.0, phase_progress)
elif global_time < EXPLOSION_PHASE + SPIRAL_PHASE:
# Spiral phase - crystalline structure
phase_progress = (global_time - EXPLOSION_PHASE) / SPIRAL_PHASE
formation_factor = 1.0
else:
# Collapse phase - singularity
phase_progress = (global_time - EXPLOSION_PHASE - SPIRAL_PHASE) / COLLAPSE_PHASE
formation_factor = 1.0 - smoothstep(0.0, 0.8, phase_progress)
# Rotation angles with crystalline angular jumps
if global_time < EXPLOSION_PHASE + SPIRAL_PHASE:
rot_y = global_time * 1.5 * 2 * math.pi # faster rotation during spiral
rot_x = math.sin(global_time * math.pi) * 0.4
rot_z = global_time * 0.5 * 2 * math.pi
else:
# Accelerating rotation during collapse
collapse_speed = 1.0 + 5.0 * phase_progress
rot_y = global_time * 1.5 * 2 * math.pi * collapse_speed
rot_x = math.sin(global_time * math.pi) * 0.4
rot_z = global_time * 0.5 * 2 * math.pi * collapse_speed
frame = splv.Frame(GRID, GRID, GRID)
for i in range(COUNT):
if global_time < EXPLOSION_PHASE:
# Explosion phase - transition from random to spiral
spiral_pos = crystalline_spiral_pos(t_vals[i], global_time)
spiral_pos = rotate_y(spiral_pos, rot_y)
spiral_pos = rotate_x(spiral_pos, rot_x)
spiral_pos = rotate_z(spiral_pos, rot_z)
target_pos = CENTER + spiral_pos
# Explosive motion
explosion_pos = explosion_positions[i] + explosion_velocities[i] * frame_idx * 2
# Interpolate from explosion to formation
world_pos = explosion_pos * (1 - formation_factor) + target_pos * formation_factor
elif global_time < EXPLOSION_PHASE + SPIRAL_PHASE:
# Spiral phase - crystalline structure
spiral_pos = crystalline_spiral_pos(t_vals[i], global_time)
# Apply rotations
rotated_pos = rotate_y(spiral_pos, rot_y)
rotated_pos = rotate_x(rotated_pos, rot_x)
rotated_pos = rotate_z(rotated_pos, rot_z)
world_pos = CENTER + rotated_pos
else:
# Collapse phase - singularity
spiral_pos = crystalline_spiral_pos(t_vals[i], global_time)
rotated_pos = rotate_y(spiral_pos, rot_y)
rotated_pos = rotate_x(rotated_pos, rot_x)
rotated_pos = rotate_z(rotated_pos, rot_z)
spiral_world_pos = CENTER + rotated_pos
# Collapse toward center with acceleration
collapse_acceleration = phase_progress * phase_progress
world_pos = spiral_world_pos * (1 - collapse_acceleration) + CENTER * collapse_acceleration
# Add energy discharge effect near the end
if phase_progress > 0.8:
energy_factor = (phase_progress - 0.8) / 0.2
energy_radius = 20 * (1 - energy_factor)
energy_angle = i * 0.1 + frame_idx * 0.3
energy_offset = np.array([
energy_radius * math.cos(energy_angle),
energy_radius * math.sin(energy_angle * 1.3),
energy_radius * math.sin(energy_angle * 0.7)
])
world_pos = CENTER + energy_offset * math.sin(frame_idx * 0.5)
x, y, z = world_pos.astype(int)
if 0 <= x < GRID and 0 <= y < GRID and 0 <= z < GRID:
# Color based on position along spiral and phase
hue_shift = 0.0 # Initialize hue_shift
if global_time < EXPLOSION_PHASE:
# Explosion colors - hot oranges and reds
hue_base = 0.05 + 0.1 * (i / COUNT)
hue_shift = global_time * 0.3
saturation = 0.9 + 0.1 * math.sin(global_time * 10 + i * 0.1)
brightness = 0.7 + 0.3 * formation_factor
elif global_time < EXPLOSION_PHASE + SPIRAL_PHASE:
# Enhanced crystalline colors - flowing rainbow with crystalline accents
# Base hue flows along the spiral path
hue_base = (i / COUNT) * 0.8 + global_time * 0.3 # flowing rainbow
hue_shift = 0.0 # Already included in hue_base
# Add crystalline facet highlights
segment = int((t_vals[i] % (2 * math.pi)) / (2 * math.pi / SEGMENTS))
facet_highlight = 0.1 * math.sin(global_time * 4 * math.pi + segment * 2)
# Dynamic saturation based on spiral position
spiral_phase = (t_vals[i] / (2 * math.pi * TURNS)) % 1.0
saturation = 0.7 + 0.3 * math.sin(spiral_phase * 4 * math.pi + global_time * 3 * math.pi)
# Pulsing brightness with harmonic variations
brightness_base = 0.8 + 0.2 * math.sin(global_time * 2 * math.pi + i * 0.1)
brightness_detail = 0.1 * math.sin(global_time * 6 * math.pi + t_vals[i] * 3)
brightness = max(0.0, min(1.0, brightness_base + brightness_detail + facet_highlight))
else:
# Collapse colors - bright white/energy discharge
if phase_progress > 0.8:
# Energy discharge - white hot
hue_base = 0.0
hue_shift = 0.0
saturation = 0.2
brightness = 1.0
else:
# Collapsing - intense colors
hue_base = 0.8 + 0.2 * (i / COUNT) # magenta range
hue_shift = global_time * 0.5
saturation = 1.0
brightness = 0.9 + 0.1 * math.sin(frame_idx * 0.2)
hue = (hue_base + hue_shift) % 1.0
color = hsv_bytes(hue, saturation, brightness)
# Enhanced particle size during key moments
if global_time < EXPLOSION_PHASE and formation_factor < 0.5:
# Larger particles during explosion
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:
frame.set_voxel(nx, ny, nz, color)
elif global_time > EXPLOSION_PHASE + SPIRAL_PHASE and phase_progress > 0.8:
# Energy discharge effect
energy_size = int(3 * (1 - (phase_progress - 0.8) / 0.2))
for dx in range(-energy_size, energy_size + 1):
for dy in range(-energy_size, energy_size + 1):
for dz in range(-energy_size, energy_size + 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) <= energy_size):
bright_color = hsv_bytes(hue, saturation * 0.5, brightness)
frame.set_voxel(nx, ny, nz, bright_color)
else:
# Normal particle
frame.set_voxel(x, y, z, color)
enc.encode(frame)
# Progress indicator
if frame_idx % FPS == 0:
seconds_done = frame_idx // FPS + 1
phase_name = "Explosion" if global_time < EXPLOSION_PHASE else \
"Crystalline Spiral" if global_time < EXPLOSION_PHASE + SPIRAL_PHASE else \
"Collapse"
print(f" second {seconds_done} / {DURATION} - {phase_name}")
enc.finish()
print("Done. Saved", OUTPUT)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment