Skip to content

Instantly share code, notes, and snippets.

@DanielHabib
Created August 28, 2025 03:27
Show Gist options
  • Select an option

  • Save DanielHabib/4c990ce5be35fc7b9fdc5a77d3aaaf11 to your computer and use it in GitHub Desktop.

Select an option

Save DanielHabib/4c990ce5be35fc7b9fdc5a77d3aaaf11 to your computer and use it in GitHub Desktop.
"""
SpatialStudio Text Feature Showcase
===================================
A vibrant and dynamic demonstration of the text rendering capabilities
in SpatialStudio. This showcase demonstrates:
- Animated titles with scaling effects
- Rainbow color cycling text
- Multi-directional text (x, y, and z axes)
- Various text scales and sizes
- Dynamic counters and live updates
- Creative text layouts and positioning
- Multi-line text capabilities
- Colorful text effects and animations
The result is a lively, fun spatial that shows rather than tells
the creative possibilities of text in 3D voxel space!
"""
from spatialstudio import splv
import math
import colorsys
import numpy as np
def main():
# Setup - larger dimensions for better text visibility
W, H, D = 256, 256, 256
framerate = 30.0
total_duration = 26.0 # 26 second showcase with bouncing letters scene
total_frames = int(framerate * total_duration)
# Load background music and get exact parameters from the MP3
print("Loading background music...")
audio_path = "background_music.mp3"
from pydub import AudioSegment
audio = AudioSegment.from_file(audio_path)
channels = audio.channels
sample_rate = audio.frame_rate
bit_depth = audio.sample_width * 8 # bytes to bits
# Get raw PCM samples
audio_samples = audio.get_array_of_samples()
audio_buf = list(audio_samples)
# Audio params: (channels, sample_rate, bytes_per_sample)
audio_params = (channels, sample_rate, 2) # 2 bytes per sample for 16-bit audio
print(f"Audio loaded: {len(audio_buf)} samples, {channels} channel(s), {sample_rate}Hz, {bit_depth}-bit")
# Calculate bytes per frame (16-bit samples = 2 bytes per sample)
samples_per_frame = int(sample_rate / framerate)
bytes_per_frame = samples_per_frame * 2 # 2 bytes per 16-bit sample
print(f"Audio: {samples_per_frame} samples per frame ({bytes_per_frame} bytes)")
# Create encoder with audio parameters
encoder = splv.Encoder(
W, H, D,
framerate=framerate,
outputPath="text_showcase.splv",
audioParams=audio_params
)
print(f"Creating {total_frames} frames for text showcase...")
for frame_num in range(total_frames):
frame = splv.Frame(W, H, D)
t = frame_num / framerate # time in seconds
progress = frame_num / total_frames # 0 to 1
# Create different scenes based on time
if t < 3.0:
# Scene 1: Animated Title Entrance
create_title_scene(frame, t, W, H, D)
elif t < 6.0:
# Scene 2: Rainbow Color Cycling
create_rainbow_scene(frame, t - 3.0, W, H, D)
elif t < 9.0:
# Scene 3: Multi-directional Text Dance
create_directional_scene(frame, t - 6.0, W, H, D)
elif t < 12.0:
# Scene 4: Scale Showcase
create_scale_scene(frame, t - 9.0, W, H, D)
elif t < 15.0:
# Scene 5: Bouncing Letters
create_bouncing_letters_scene(frame, t - 12.0, W, H, D)
elif t < 18.0:
# Scene 6: Dynamic Counters and Updates
create_dynamic_scene(frame, t - 15.0, W, H, D, frame_num)
elif t < 21.0:
# Scene 7: Creative Layouts
create_layout_scene(frame, t - 18.0, W, H, D)
elif t < 23.0:
# Scene 8: Grand Finale
create_finale_scene(frame, t - 21.0, W, H, D, frame_num)
else:
# Scene 9: Call-to-Action Scene
create_cta_scene(frame, t - 23.0, W, H, D)
encoder.encode(frame)
# Encode audio for this frame
start_sample = frame_num * samples_per_frame
end_sample = min(start_sample + samples_per_frame, len(audio_buf))
if start_sample < len(audio_buf):
frame_audio = audio_buf[start_sample:end_sample]
# Pad with silence if we don't have enough samples
while len(frame_audio) < samples_per_frame:
frame_audio.append(0)
# Convert to bytes then to list (following fire.py approach)
frame_audio_np = np.array(frame_audio, dtype=np.int16)
frame_audio_bytes = frame_audio_np.tobytes()
frame_audio_list = list(frame_audio_bytes)
encoder.encode_audio(frame_audio_list)
else:
# Silence if we've run out of audio (need bytes_per_frame bytes)
encoder.encode_audio([0] * bytes_per_frame)
# Progress indicator
if frame_num % 60 == 0:
print(f"Progress: {frame_num}/{total_frames} frames ({progress*100:.1f}%)")
print("Finalizing spatial...")
encoder.finish()
print("Text showcase complete! 🎉")
def create_title_scene(frame, t, W, H, D):
"""Animated title entrance with scaling and movement"""
# Animated scaling for dramatic entrance
scale = max(1, int(4 - t * 1.2)) # Start big, shrink down
if scale > 4:
scale = 4
# Bouncing effect
bounce = int(abs(math.sin(t * 4)) * 10)
# Main title
title_y = H // 2 + 40 + bounce
start_x = 20
frame.write_string(
text="SPATIAL STUDIO",
startPos=(start_x, title_y, D // 4),
voxel=(255, 100, 255), # Bright magenta
outlineVoxel=(255, 255, 255), # White outline
axis="x",
scale=scale,
maxWidth=W - start_x # Keep text within frame bounds
)
# Subtitle appears after 1 second
if t > 1.0:
subtitle_scale = min(2, int((t - 1.0) * 2) + 1)
subtitle_start_x = 40
frame.write_string(
text="TEXT FEATURES!",
startPos=(subtitle_start_x, title_y - 30, D // 3),
voxel=(100, 255, 255), # Cyan
outlineVoxel=(0, 0, 100), # Dark blue outline
axis="x",
scale=subtitle_scale,
maxWidth=W - subtitle_start_x # Keep subtitle within frame bounds
)
# Sparkle effects around text
for i in range(10):
spark_x = int(50 + i * 15 + math.sin(t * 3 + i) * 10)
spark_y = int(title_y + math.cos(t * 2 + i) * 20)
spark_z = int(D // 2 + math.sin(t * 4 + i) * 5)
if 0 <= spark_x < W and 0 <= spark_y < H and 0 <= spark_z < D:
frame[spark_x, spark_y, spark_z] = (255, 255, 100) # Yellow sparkles
def create_rainbow_scene(frame, t, W, H, D):
"""Rainbow color cycling demonstration"""
# Multiple lines of text with different rainbow phases
texts = ["RAINBOW", "COLORS", "CYCLING", "AMAZING!"]
for i, text in enumerate(texts):
# Each line has a different phase offset
hue_offset = i * 0.25 + t * 0.3
# Convert HSV to RGB for rainbow effect
r, g, b = colorsys.hsv_to_rgb(hue_offset % 1.0, 1.0, 1.0)
text_color = (int(r * 255), int(g * 255), int(b * 255))
# Outline color is complementary
outline_r, outline_g, outline_b = colorsys.hsv_to_rgb((hue_offset + 0.5) % 1.0, 0.8, 0.8)
outline_color = (int(outline_r * 255), int(outline_g * 255), int(outline_b * 255))
# Wavy movement
wave_offset = int(math.sin(t * 2 + i * 0.5) * 30)
frame.write_string(
text=text,
startPos=(50 + wave_offset, H - 50 - i * 35, D // 2),
voxel=text_color,
outlineVoxel=outline_color,
axis="x",
scale=2
)
def create_directional_scene(frame, t, W, H, D):
"""Multi-directional text demonstration"""
# Horizontal text (x-axis)
frame.write_string(
text="HORIZONTAL",
startPos=(20, H // 2 + 50, D // 2),
voxel=(255, 150, 0), # Orange
outlineVoxel=(100, 0, 0), # Dark red
axis="x",
scale=2
)
# Vertical text (z-axis) - appears to move toward/away from camera
z_pos = int(D // 2 + math.sin(t * 2) * 50)
frame.write_string(
text="DEPTH",
startPos=(W // 2, H // 2, max(10, min(D-50, z_pos))),
voxel=(0, 255, 150), # Green-cyan
outlineVoxel=(0, 100, 50), # Dark green
axis="z",
scale=3
)
# Flipping text effect
use_flip = int(t * 2) % 2 == 0
frame.write_string(
text="FLIPPING",
startPos=(30, H // 2 - 50, D // 2),
voxel=(255, 0, 150), # Pink
outlineVoxel=(100, 0, 100), # Dark purple
axis="x",
flip=use_flip,
scale=2
)
def create_scale_scene(frame, t, W, H, D):
"""Different text scales showcase"""
scales = [1, 2, 3, 4]
texts = ["TINY", "SMALL", "BIG", "HUGE"]
colors = [(255, 255, 255), (255, 200, 0), (255, 100, 0), (255, 0, 0)]
for i, (scale, text, color) in enumerate(zip(scales, texts, colors)):
# Staggered animation entrance
if t > i * 0.5:
y_pos = H - 60 - i * (20 + scale * 8)
# Pulsing effect
pulse_scale = scale + int(math.sin(t * 4 + i) * 0.5)
pulse_scale = max(1, pulse_scale)
start_x = 30
frame.write_string(
text=text,
startPos=(start_x, y_pos, D // 2),
voxel=color,
outlineVoxel=(0, 0, 0), # Black outline
axis="x",
scale=pulse_scale,
maxWidth=W - start_x # Keep all scale text within frame bounds
)
def create_dynamic_scene(frame, t, W, H, D, frame_num):
"""Dynamic counters and live text updates"""
# Animated counter
counter_value = int(t * 10) % 100
counter_text = f"COUNT: {counter_value:03d}"
frame.write_string(
text=counter_text,
startPos=(20, H - 40, D // 4),
voxel=(0, 255, 0), # Green
outlineVoxel=(0, 0, 0), # Black outline
axis="x",
scale=2
)
# Frame number display
frame_text = f"FRAME: {frame_num:04d}"
frame.write_string(
text=frame_text,
startPos=(20, H - 80, D // 4),
voxel=(255, 255, 0), # Yellow
outlineVoxel=(100, 100, 0), # Dark yellow
axis="x",
scale=1
)
# Progress bar made of text
progress = int((t % 3.0) / 3.0 * 20) # 0 to 20
progress_text = "=" * progress + "-" * (20 - progress)
frame.write_string(
text=progress_text,
startPos=(20, H // 2, D // 4),
voxel=(0, 150, 255), # Blue
outlineVoxel=(0, 50, 100), # Dark blue
axis="x",
scale=1
)
# Status text that changes
statuses = ["LOADING", "PROCESSING", "RENDERING", "COMPLETE!"]
status_idx = int(t * 2) % len(statuses)
frame.write_string(
text=statuses[status_idx],
startPos=(20, H // 2 - 40, D // 4),
voxel=(255, 100, 255), # Magenta
outlineVoxel=(100, 0, 100), # Dark purple
axis="x",
scale=2
)
def create_layout_scene(frame, t, W, H, D):
"""Creative text layouts and wrapping"""
# Long text that wraps
long_text = "THIS DEMONSTRATES TEXT WRAPPING WHEN THE LINE IS TOO LONG FOR THE SPECIFIED WIDTH!"
frame.write_string(
text=long_text,
startPos=(10, H - 50, 0),
voxel=(255, 255, 255), # White
outlineVoxel=(100, 100, 100), # Gray
axis="x",
scale=1,
maxWidth=180 # Force wrapping
)
# Circular text arrangement (simulated)
center_x, center_y = W // 2, H // 2
radius = 60
text = "CREATIVE LAYOUTS"
for i, char in enumerate(text):
if char != ' ': # Skip spaces
angle = (i / len(text)) * 2 * math.pi + t
x = int(center_x + math.cos(angle) * radius)
y = int(center_y + math.sin(angle) * radius)
# Color based on position in circle
hue = (i / len(text)) % 1.0
r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
color = (int(r * 255), int(g * 255), int(b * 255))
if 0 <= x < W-20 and 0 <= y < H-20:
frame.write_string(
text=char,
startPos=(x, y, D // 2),
voxel=color,
outlineVoxel=(0, 0, 0),
axis="x",
scale=2
)
def create_finale_scene(frame, t, W, H, D, frame_num):
"""Grand finale combining all features"""
# Multiple animated text elements
# Main title with rainbow and scaling
scale = int(2 + math.sin(t * 3) * 1)
hue = (t * 0.5) % 1.0
r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
title_color = (int(r * 255), int(g * 255), int(b * 255))
frame.write_string(
text="TEXT POWER!",
startPos=(W // 2 - 80, H // 2 + 60, D // 3),
voxel=title_color,
outlineVoxel=(255, 255, 255),
axis="x",
scale=scale
)
# Rotating subtitle
use_flip = int(t * 4) % 2 == 0
frame.write_string(
text="AMAZING FEATURES",
startPos=(W // 2 - 60, H // 2 + 20, D // 2),
voxel=(255, 255, 100),
outlineVoxel=(100, 100, 0),
axis="x",
flip=use_flip,
scale=1
)
# Dancing numbers
for i in range(5):
num_text = str(i + 1)
bounce = int(math.sin(t * 5 + i) * 15)
x_pos = 50 + i * 30
y_pos = H // 2 - 30 + bounce
frame.write_string(
text=num_text,
startPos=(x_pos, y_pos, D // 2),
voxel=(255, 150, 255),
outlineVoxel=(100, 0, 100),
axis="x",
scale=3
)
# Depth text moving in z
z_pos = int(D // 2 + math.sin(t * 2) * 80)
frame.write_string(
text="3D SPACE",
startPos=(W // 2 - 30, H // 2 - 80, max(10, min(D-50, z_pos))),
voxel=(100, 255, 255),
outlineVoxel=(0, 100, 100),
axis="z",
scale=2
)
# Final message
if t > 1.5:
frame.write_string(
text="SPATIALSTUDIO ROCKS!",
startPos=(20, 30, D // 4),
voxel=(255, 255, 255),
outlineVoxel=(255, 0, 0),
axis="x",
scale=2
)
def create_bouncing_letters_scene(frame, t, W, H, D):
"""Individual letters bouncing independently while maintaining word legibility"""
# The word to display with bouncing letters
word = "BOUNCE"
letter_count = len(word)
# Calculate spacing and starting position for centered word
letter_spacing = 25 # Wider spacing for better readability
start_x = W // 2 - (letter_count * letter_spacing) // 2
base_y = H // 2
base_z = D // 4
for i, letter in enumerate(word):
# Each letter has its own subtle bounce pattern
bounce_phase = i * 0.5 # Less staggered for better readability
bounce_freq = 3.0 + (i % 2) * 0.3 # Subtle frequency variation
bounce_height = 20 + (i % 3) * 8 # Moderate bounce height
# Vertical bounce - keep it subtle so word stays readable
bounce_y = int(bounce_height * abs(math.sin(t * bounce_freq + bounce_phase)))
# Very subtle horizontal wiggle to maintain alignment
wiggle_x = int(3 * math.sin(t * 2 + bounce_phase))
# Minimal Z-axis movement to keep letters aligned
rotate_z = int(8 * math.sin(t * 1.8 + bounce_phase))
# Color cycling - each letter gets a different hue but stays vibrant
hue_offset = i * 0.15
r = int(128 + 127 * math.sin(t * 1.5 + hue_offset))
g = int(128 + 127 * math.sin(t * 1.5 + hue_offset + 2))
b = int(128 + 127 * math.sin(t * 1.5 + hue_offset + 4))
# Position for this letter - maintain word structure
letter_x = start_x + wiggle_x
letter_y = base_y - bounce_y
letter_z = base_z + i * letter_spacing + rotate_z
# Consistent scale with subtle bounce variation
bounce_scale = int(3 + bounce_y / 30) # Scale 3-4 range
# Draw the letter with strong outline for readability
frame.write_string(
text=letter,
startPos=(letter_x, letter_y, letter_z),
voxel=(r, g, b),
outlineVoxel=(255, 255, 255),
axis="x",
scale=bounce_scale
)
# Add title text that appears after 1 second
if t > 1.0:
# Gentle pulsing title
title_scale = 2
frame.write_string(
text="Individual Letter Physics!",
startPos=(W // 4, 30, D // 4),
voxel=(255, 255, 100),
outlineVoxel=(150, 150, 0),
axis="x",
scale=title_scale
)
def create_cta_scene(frame, t, W, H, D):
"""Dedicated call-to-action scene with clean pulsing rainbow text"""
# Main CTA - "TRY IT TODAY!" with dramatic pulsing
pulse_scale = int(3 + math.sin(t * 6) * 1.2) # More dramatic pulsing
rainbow_r = int(128 + 127 * math.sin(t * 4))
rainbow_g = int(128 + 127 * math.sin(t * 4 + 2))
rainbow_b = int(128 + 127 * math.sin(t * 4 + 4))
start_pos = (W // 2 - 65, H // 2 - 20, D // 4)
# Position it center screen
frame.write_string(
text="TRY IT TODAY!",
startPos=start_pos,
voxel=(rainbow_r, rainbow_g, rainbow_b),
outlineVoxel=(255, 255, 255),
axis="x",
scale=pulse_scale,
maxWidth=W - start_pos[0]
)
# Subtitle appears after a moment
if t > 0.8:
# Gentle bounce effect for subtitle
bounce_offset = int(5 * math.sin(t * 3))
subtitle_start_pos = (W // 3, H // 2 + 40 + bounce_offset, D // 4 + 30)
frame.write_string(
text="Your voxels are waiting!",
startPos=subtitle_start_pos,
voxel=(150, 255, 150),
outlineVoxel=(0, 150, 0),
axis="x",
scale=2,
maxWidth=W - subtitle_start_pos[0]
)
# Fun sparkle effects around the text
if t > 1.5:
for i in range(8):
sparkle_x = int(W // 2 + 120 * math.cos(t * 3 + i))
sparkle_y = int(H // 2 + 60 * math.sin(t * 2 + i))
sparkle_z = int(D // 2 + 40 * math.sin(t * 4 + i))
# Keep sparkles in bounds
if 10 < sparkle_x < W-10 and 10 < sparkle_y < H-10 and 10 < sparkle_z < D-10:
sparkle_color = int(200 + 55 * math.sin(t * 8 + i))
frame.set_voxel(sparkle_x, sparkle_y, sparkle_z, (sparkle_color, sparkle_color, 255))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment