-
-
Save DanielHabib/4c990ce5be35fc7b9fdc5a77d3aaaf11 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
| """ | |
| 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