Skip to content

Instantly share code, notes, and snippets.

@DanielHabib
Created September 11, 2025 03:48
Show Gist options
  • Select an option

  • Save DanielHabib/707a7b2f4b9f6b80fa383572dab6417b to your computer and use it in GitHub Desktop.

Select an option

Save DanielHabib/707a7b2f4b9f6b80fa383572dab6417b to your computer and use it in GitHub Desktop.
from spatialstudio import splv
import math
import time
import random
def create_physics_showcase():
"""
Create a clean physics showcase with bouncing balls cascading down
multiple angled planes - demonstrating realistic gravity, collisions, and energy transfer!
"""
# Video parameters
W, H, D = 400, 256, 64
framerate = 60.0 # Higher framerate for smoother physics
total_duration = 25.0 # 25 seconds of physics demonstration
total_frames = int(total_duration * framerate)
encoder = splv.Encoder(
W, H, D,
framerate=framerate,
outputPath="physics_showcase.splv"
)
print(f"Creating Enhanced Physics Showcase: {total_frames} frames at {framerate} FPS")
# Initialize the physics world
balls = initialize_balls(W, H, D)
planes = create_collision_planes(W, H, D)
particle_effects = [] # For collision sparks and effects
for frame_num in range(total_frames):
frame = splv.Frame(W, H, D)
# Calculate time-based variables
t = frame_num / framerate
# Skip background gradient for cleaner look
# add_background_gradient(frame, t, W, H, D)
# Draw the collision planes with simple, bold colors
draw_simple_planes(frame, planes, W, H, D)
# Update ball physics
update_ball_physics(balls, planes, W, H, D, particle_effects)
# Update and draw particle effects
update_particle_effects(particle_effects)
draw_particle_effects(frame, particle_effects)
# Draw balls with enhanced motion trails
draw_enhanced_balls_with_trails(frame, balls, t)
# Spawn new balls more frequently with variety
spawn_frequency = 45 if frame_num < total_frames * 0.3 else 60 # More balls at start
if frame_num % spawn_frequency == 0 and frame_num < total_frames * 0.85:
spawn_new_ball(balls, W, H, D, frame_num)
# Occasionally spawn multiple balls at once for excitement
if frame_num % 300 == 150: # Every 5 seconds, offset timing
for _ in range(3):
spawn_new_ball(balls, W, H, D, frame_num)
encoder.encode(frame)
if frame_num % 180 == 0:
progress = frame_num / total_frames
print(f"Progress: {frame_num}/{total_frames} ({progress*100:.1f}%)")
encoder.finish()
print("Physics showcase completed!")
def initialize_balls(W, H, D):
"""Initialize the physics balls with realistic properties"""
balls = []
# Simple, clean primary colors that will show up clearly
colors = [
(255, 0, 0), # Pure Red
(0, 255, 0), # Pure Green
(0, 0, 255), # Pure Blue
(255, 255, 0), # Yellow
(255, 0, 255), # Magenta
(0, 255, 255), # Cyan
(255, 128, 0), # Orange
(128, 0, 255), # Purple
(255, 192, 203), # Pink
(0, 128, 255), # Sky Blue
]
# Start with more balls at different positions
for i in range(5):
ball = {
'x': W * 0.15 + i * W * 0.15 + random.uniform(-20, 20),
'y': H - 10 - i * 25 + random.uniform(-10, 10),
'z': D * 0.2 + i * D * 0.15 + random.uniform(-5, 5),
'vx': random.uniform(-2, 2),
'vy': random.uniform(-1, 1),
'vz': random.uniform(-1, 1),
'radius': random.uniform(2.5, 7),
'color': colors[i % len(colors)],
'bounce_damping': random.uniform(0.65, 0.9),
'trail': [], # Store recent positions for trail effect
'active': True,
'glow_intensity': random.uniform(0.8, 1.2),
'spin': random.uniform(-0.1, 0.1) # For visual rotation effect
}
balls.append(ball)
return balls
def create_collision_planes(W, H, D):
"""Create a series of stepped collision planes"""
planes = []
# Create 4 stepped planes with bold, simple colors
plane_colors = [
(200, 200, 200), # Light Gray
(160, 160, 160), # Medium Gray
(120, 120, 120), # Dark Gray
(80, 80, 80), # Darker Gray
]
for i in range(4):
plane = {
'y': H - 40 - i * 45, # Stepping down
'x_start': 50 + i * 60,
'x_end': 200 + i * 60,
'z_start': 10,
'z_end': D - 10,
'angle': 0, # Flat planes for now
'color': plane_colors[i]
}
planes.append(plane)
# Add one angled ramp at the end with a bold blue color
ramp = {
'y': 30,
'x_start': 280,
'x_end': 350,
'z_start': 15,
'z_end': D - 15,
'angle': -15, # Sloped downward
'color': (0, 100, 200) # Bold Blue
}
planes.append(ramp)
return planes
def add_background_gradient(frame, t, W, H, D):
"""Add a subtle animated background gradient"""
for y in range(0, H, 8): # Sample every 8th row for performance
for x in range(0, W, 12): # Sample every 12th column
for z in range(0, D, 6): # Sample every 6th depth
# Create a subtle moving gradient
gradient_factor = (y / H) * 0.3 + math.sin(t * 0.5 + x * 0.01) * 0.1
color_intensity = max(0, int(gradient_factor * 40))
bg_color = (
max(0, min(255, color_intensity)),
max(0, min(255, color_intensity // 2)),
max(0, min(255, color_intensity + 10))
)
if 0 <= x < W and 0 <= y < H and 0 <= z < D:
frame[x, y, z] = bg_color
def draw_simple_planes(frame, planes, W, H, D):
"""Draw collision planes with simple, bold colors"""
for plane in planes:
# Draw main plane surface with solid color
for x in range(int(plane['x_start']), int(plane['x_end'])):
for z in range(int(plane['z_start']), int(plane['z_end'])):
if 0 <= x < W and 0 <= z < D:
y = int(plane['y'])
if 0 <= y < H:
frame[x, y, z] = plane['color']
# Add simple thickness
if y - 1 >= 0:
darker_color = tuple(max(0, c - 40) for c in plane['color'])
frame[x, y - 1, z] = darker_color
def draw_enhanced_planes(frame, planes, W, H, D, t):
"""Draw collision planes with enhanced visuals and animation"""
for i, plane in enumerate(planes):
# Add subtle animation to plane colors
color_pulse = math.sin(t * 2 + i * 0.5) * 0.2 + 0.8
animated_color = tuple(int(c * color_pulse) for c in plane['color'])
# Draw main plane surface
for x in range(int(plane['x_start']), int(plane['x_end'])):
for z in range(int(plane['z_start']), int(plane['z_end'])):
if 0 <= x < W and 0 <= z < D:
y = int(plane['y'])
if 0 <= y < H:
# Add some texture variation
texture_variation = int(math.sin(x * 0.3) * math.cos(z * 0.3) * 20)
textured_color = tuple(max(0, min(255, c + texture_variation)) for c in animated_color)
frame[x, y, z] = textured_color
# Add thickness with shading
for thickness in range(1, 3):
if y - thickness >= 0:
shade_factor = 1.0 - thickness * 0.3
shaded_color = tuple(int(c * shade_factor) for c in textured_color)
frame[x, y - thickness, z] = shaded_color
def draw_planes(frame, planes, W, H, D):
"""Draw all collision planes"""
for plane in planes:
# Draw flat rectangular planes
for x in range(int(plane['x_start']), int(plane['x_end'])):
for z in range(int(plane['z_start']), int(plane['z_end'])):
if 0 <= x < W and 0 <= z < D:
y = int(plane['y'])
if 0 <= y < H:
frame[x, y, z] = plane['color']
# Add some thickness
if y > 0:
frame[x, y-1, z] = tuple(c // 2 for c in plane['color'])
def update_ball_physics(balls, planes, W, H, D, particle_effects):
"""Update physics for all balls"""
gravity = -0.3
air_resistance = 0.99
for ball in balls:
if not ball['active']:
continue
# Apply gravity
ball['vy'] += gravity
# Apply air resistance
ball['vx'] *= air_resistance
ball['vz'] *= air_resistance
# Update position
ball['x'] += ball['vx']
ball['y'] += ball['vy']
ball['z'] += ball['vz']
# Store trail positions
ball['trail'].append((ball['x'], ball['y'], ball['z']))
if len(ball['trail']) > 15: # Keep last 15 positions
ball['trail'].pop(0)
# Check collision with planes
check_plane_collisions(ball, planes, particle_effects)
# Boundary collisions
if ball['x'] <= ball['radius'] or ball['x'] >= W - ball['radius']:
ball['vx'] *= -ball['bounce_damping']
ball['x'] = max(ball['radius'], min(W - ball['radius'], ball['x']))
if ball['z'] <= ball['radius'] or ball['z'] >= D - ball['radius']:
ball['vz'] *= -ball['bounce_damping']
ball['z'] = max(ball['radius'], min(D - ball['radius'], ball['z']))
# Remove balls that fall too far
if ball['y'] < -50:
ball['active'] = False
def check_plane_collisions(ball, planes, particle_effects):
"""Check and handle collisions between ball and planes"""
for plane in planes:
# Check if ball is within plane bounds
if (plane['x_start'] <= ball['x'] <= plane['x_end'] and
plane['z_start'] <= ball['z'] <= plane['z_end']):
plane_y = plane['y']
# Check if ball is colliding with plane from above
if (ball['y'] - ball['radius'] <= plane_y and
ball['y'] - ball['radius'] >= plane_y - 5 and
ball['vy'] < 0): # Moving downward
# Create collision particles
impact_strength = abs(ball['vy'])
for _ in range(int(impact_strength * 3)):
particle = {
'x': ball['x'] + random.uniform(-ball['radius'], ball['radius']),
'y': plane_y + 1,
'z': ball['z'] + random.uniform(-ball['radius'], ball['radius']),
'vx': random.uniform(-impact_strength, impact_strength),
'vy': random.uniform(0, impact_strength * 2),
'vz': random.uniform(-impact_strength * 0.5, impact_strength * 0.5),
'color': ball['color'],
'life': 30, # Frames to live
'max_life': 30
}
particle_effects.append(particle)
# Bounce off the plane
ball['vy'] *= -ball['bounce_damping']
ball['y'] = plane_y + ball['radius'] + 1
# Add some horizontal velocity based on impact
impact_factor = abs(ball['vy']) * 0.15
ball['vx'] += random.uniform(-impact_factor, impact_factor)
ball['vz'] += random.uniform(-impact_factor * 0.5, impact_factor * 0.5)
break # Only collide with one plane per frame
def draw_balls_with_trails(frame, balls, t):
"""Draw balls with motion trails"""
for ball in balls:
if not ball['active']:
continue
# Draw trail
for i, (tx, ty, tz) in enumerate(ball['trail']):
if i < len(ball['trail']) - 1: # Don't draw trail at current position
trail_alpha = i / len(ball['trail'])
trail_color = tuple(int(c * trail_alpha * 0.5) for c in ball['color'])
# Draw trail point
tx, ty, tz = int(tx), int(ty), int(tz)
if 0 <= tx < frame.get_dims()[0] and 0 <= ty < frame.get_dims()[1] and 0 <= tz < frame.get_dims()[2]:
frame[tx, ty, tz] = trail_color
# Draw main ball
draw_sphere(frame, ball['x'], ball['y'], ball['z'], ball['radius'], ball['color'])
def update_particle_effects(particle_effects):
"""Update all particle effects"""
for particle in particle_effects[:]:
# Apply gravity to particles
particle['vy'] -= 0.1
# Update position
particle['x'] += particle['vx']
particle['y'] += particle['vy']
particle['z'] += particle['vz']
# Apply air resistance
particle['vx'] *= 0.98
particle['vz'] *= 0.98
# Decrease life
particle['life'] -= 1
# Remove dead particles
if particle['life'] <= 0:
particle_effects.remove(particle)
def draw_particle_effects(frame, particle_effects):
"""Draw all particle effects"""
W, H, D = frame.get_dims()
for particle in particle_effects:
x, y, z = int(particle['x']), int(particle['y']), int(particle['z'])
if 0 <= x < W and 0 <= y < H and 0 <= z < D:
# Fade particle based on remaining life
life_factor = particle['life'] / particle['max_life']
faded_color = tuple(max(0, min(255, int(c * life_factor))) for c in particle['color'])
frame[x, y, z] = faded_color
def draw_enhanced_balls_with_trails(frame, balls, t):
"""Draw balls with enhanced motion trails and glow effects"""
for ball in balls:
if not ball['active']:
continue
# Draw enhanced trail with varying thickness and fun colors
for i, (tx, ty, tz) in enumerate(ball['trail']):
if i < len(ball['trail']) - 1: # Don't draw trail at current position
trail_alpha = (i / len(ball['trail'])) ** 0.7 # Non-linear fade
trail_intensity = trail_alpha * ball['glow_intensity']
# Create complementary trail colors that shift through the spectrum
trail_shift = (i / len(ball['trail'])) * 0.5 # Color shift factor
r, g, b = ball['color']
# Create rainbow-like trail effect
trail_r = int((r * (1 - trail_shift) + g * trail_shift) * trail_intensity * 0.8)
trail_g = int((g * (1 - trail_shift) + b * trail_shift) * trail_intensity * 0.8)
trail_b = int((b * (1 - trail_shift) + r * trail_shift) * trail_intensity * 0.8)
trail_color = (trail_r, trail_g, trail_b)
# Draw trail with slight thickness
tx, ty, tz = int(tx), int(ty), int(tz)
W, H, D = frame.get_dims()
for dx in range(-1, 2):
for dy in range(-1, 2):
for dz in range(-1, 2):
nx, ny, nz = tx + dx, ty + dy, tz + dz
if (0 <= nx < W and 0 <= ny < H and 0 <= nz < D and
abs(dx) + abs(dy) + abs(dz) <= 2): # Diamond shape
brightness = 1.0 - (abs(dx) + abs(dy) + abs(dz)) * 0.3
final_color = tuple(max(0, min(255, int(c * brightness))) for c in trail_color)
frame[nx, ny, nz] = final_color
# Draw main ball with simple, clear colors
draw_simple_sphere(frame, ball['x'], ball['y'], ball['z'], ball['radius'], ball['color'])
def draw_simple_sphere(frame, cx, cy, cz, radius, color):
"""Draw a simple, bright sphere that clearly shows the color"""
W, H, D = frame.get_dims()
for x in range(max(0, int(cx - radius)), min(W, int(cx + radius + 1))):
for y in range(max(0, int(cy - radius)), min(H, int(cy + radius + 1))):
for z in range(max(0, int(cz - radius)), min(D, int(cz + radius + 1))):
dx = x - cx
dy = y - cy
dz = z - cz
distance = math.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= radius:
# Simple brightness based on distance from center
brightness = 1.0 - (distance / radius) * 0.3 # Keep most of the color
bright_color = tuple(max(50, int(c * brightness)) for c in color) # Minimum brightness of 50
frame[x, y, z] = bright_color
def draw_enhanced_sphere(frame, cx, cy, cz, radius, color, glow_intensity, rotation):
"""Draw an enhanced sphere with glow effects"""
W, H, D = frame.get_dims()
# Draw glow effect first (larger radius, dimmer)
glow_radius = radius + 2
for x in range(max(0, int(cx - glow_radius)), min(W, int(cx + glow_radius + 1))):
for y in range(max(0, int(cy - glow_radius)), min(H, int(cy + glow_radius + 1))):
for z in range(max(0, int(cz - glow_radius)), min(D, int(cz + glow_radius + 1))):
dx = x - cx
dy = y - cy
dz = z - cz
distance = math.sqrt(dx*dx + dy*dy + dz*dz)
if radius < distance <= glow_radius:
glow_factor = 1.0 - ((distance - radius) / (glow_radius - radius))
glow_factor *= glow_intensity * 0.3
glow_color = tuple(max(0, min(255, int(c * glow_factor))) for c in color)
frame[x, y, z] = glow_color
# Draw main sphere
for x in range(max(0, int(cx - radius)), min(W, int(cx + radius + 1))):
for y in range(max(0, int(cy - radius)), min(H, int(cy + radius + 1))):
for z in range(max(0, int(cz - radius)), min(D, int(cz + radius + 1))):
dx = x - cx
dy = y - cy
dz = z - cz
distance = math.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= radius:
# Enhanced shading with rotation effect
brightness = 1.0 - (distance / radius) * 0.4
rotation_effect = math.sin(rotation + dx * 0.5) * 0.1 + 1.0
final_brightness = brightness * rotation_effect * glow_intensity
shaded_color = tuple(max(0, min(255, int(c * final_brightness))) for c in color)
frame[x, y, z] = shaded_color
def draw_sphere(frame, cx, cy, cz, radius, color):
"""Draw a sphere at the given position"""
W, H, D = frame.get_dims()
for x in range(max(0, int(cx - radius)), min(W, int(cx + radius + 1))):
for y in range(max(0, int(cy - radius)), min(H, int(cy + radius + 1))):
for z in range(max(0, int(cz - radius)), min(D, int(cz + radius + 1))):
# Calculate distance from center
dx = x - cx
dy = y - cy
dz = z - cz
distance = math.sqrt(dx*dx + dy*dy + dz*dz)
if distance <= radius:
# Add some shading based on distance from center
brightness = 1.0 - (distance / radius) * 0.3
shaded_color = tuple(int(c * brightness) for c in color)
frame[x, y, z] = shaded_color
def spawn_new_ball(balls, W, H, D, frame_num=0):
"""Spawn a new ball at a random position with enhanced variety"""
colors = [
(255, 0, 0), # Pure Red
(0, 255, 0), # Pure Green
(0, 0, 255), # Pure Blue
(255, 255, 0), # Yellow
(255, 0, 255), # Magenta
(0, 255, 255), # Cyan
(255, 128, 0), # Orange
(128, 0, 255), # Purple
(255, 192, 203), # Pink
(0, 128, 255), # Sky Blue
(128, 255, 0), # Lime
(255, 64, 0), # Red-Orange
]
# Vary spawn positions based on time for more interesting patterns
spawn_variation = math.sin(frame_num * 0.02) * W * 0.2
new_ball = {
'x': W * 0.5 + spawn_variation + random.uniform(-W * 0.3, W * 0.3),
'y': H - random.uniform(5, 15),
'z': D * 0.5 + random.uniform(-D * 0.3, D * 0.3),
'vx': random.uniform(-3, 3),
'vy': random.uniform(-2, 2),
'vz': random.uniform(-2, 2),
'radius': random.uniform(2, 8), # Larger size variety
'color': random.choice(colors),
'bounce_damping': random.uniform(0.5, 0.95), # More bounce variety
'trail': [],
'active': True,
'glow_intensity': random.uniform(0.8, 1.5),
'spin': random.uniform(-0.2, 0.2)
}
balls.append(new_ball)
if __name__ == "__main__":
start_time = time.time()
create_physics_showcase()
end_time = time.time()
print(f"Total generation time: {end_time - start_time:.2f} seconds")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment