Created
March 13, 2026 02:37
-
-
Save tiveor/0300a127852c8b1ae41772983139bfcf to your computer and use it in GitHub Desktop.
generate.py
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 | |
| """ | |
| 'The Journey of a Prompt' | |
| A creative visualization of how Claude processes and responds to prompts. | |
| Generated entirely with Python + Pillow + FFmpeg. | |
| """ | |
| import math, random, os, struct, wave, subprocess, colorsys, time | |
| from PIL import Image, ImageDraw, ImageFont | |
| random.seed(42) | |
| # === CONFIG === | |
| W, H = 1080, 1920 | |
| FPS = 30 | |
| DURATION = 65 | |
| TOTAL = FPS * DURATION | |
| DIR = "/Users/alvarotech/dev/random/claude_video" | |
| # === FONTS === | |
| def font(sz): | |
| for p in ["/System/Library/Fonts/Menlo.ttc", "/System/Library/Fonts/SFNSMono.ttf"]: | |
| if os.path.exists(p): | |
| try: | |
| return ImageFont.truetype(p, sz) | |
| except: | |
| pass | |
| return ImageFont.load_default() | |
| F_SM = font(22) | |
| F_MD = font(34) | |
| F_LG = font(52) | |
| F_XL = font(80) | |
| F_TINY = font(16) | |
| # === COLORS === | |
| CYAN = (0, 220, 255) | |
| AMBER = (255, 180, 50) | |
| PURPLE = (160, 60, 255) | |
| WHITE = (240, 240, 250) | |
| TEAL = (0, 180, 180) | |
| WARM = (255, 230, 200) | |
| # === HELPERS === | |
| def lerp(a, b, t): | |
| return a + (b - a) * max(0, min(1, t)) | |
| def ease(t): | |
| t = max(0, min(1, t)) | |
| return t * t * (3 - 2 * t) | |
| def ease_out(t): | |
| t = max(0, min(1, t)) | |
| return 1 - (1 - t) ** 3 | |
| def col_a(c, a): | |
| a = max(0, min(1, a)) | |
| return tuple(max(0, min(255, int(v * a))) for v in c) | |
| def blend(c1, c2, t): | |
| return tuple(int(lerp(c1[i], c2[i], t)) for i in range(3)) | |
| def hsv(h, s, v): | |
| r, g, b = colorsys.hsv_to_rgb(h % 1, s, v) | |
| return (int(r * 255), int(g * 255), int(b * 255)) | |
| # === PRE-COMPUTED DATA === | |
| PROMPT_TEXT = "Hazme algo increíble..." | |
| CODE_LINES = [ | |
| "def understand(prompt):", | |
| " tokens = tokenize(prompt)", | |
| " context = build_context(tokens)", | |
| " meaning = extract_intent(context)", | |
| " return meaning", | |
| "", | |
| "def think(understanding):", | |
| " paths = explore_possibilities(understanding)", | |
| " evaluated = [score(p) for p in paths]", | |
| " best = select_optimal(evaluated)", | |
| " return synthesize(best)", | |
| "", | |
| "def create(thought):", | |
| " structure = plan(thought)", | |
| " content = generate(structure)", | |
| " refined = iterate(content)", | |
| " return refined", | |
| "", | |
| "def respond(creation):", | |
| " formatted = present(creation)", | |
| " return formatted", | |
| "", | |
| "# The journey of every prompt", | |
| "prompt = receive()", | |
| "understood = understand(prompt)", | |
| "thought = think(understood)", | |
| "created = create(thought)", | |
| "response = respond(created)", | |
| "deliver(response) # ✨", | |
| ] | |
| # Stars | |
| stars = [(random.randint(0, W), random.randint(0, H), random.random() * 2 + 0.5, random.random() * math.tau) | |
| for _ in range(150)] | |
| # Network nodes | |
| random.seed(77) | |
| nodes = [] | |
| for i in range(30): | |
| angle = random.random() * math.tau | |
| dist = random.random() * 350 + 80 | |
| nodes.append((W // 2 + math.cos(angle) * dist, H // 2 + math.sin(angle) * dist)) | |
| edges = [] | |
| for i in range(len(nodes)): | |
| for j in range(i + 1, len(nodes)): | |
| dx = nodes[i][0] - nodes[j][0] | |
| dy = nodes[i][1] - nodes[j][1] | |
| if math.sqrt(dx * dx + dy * dy) < 300: | |
| edges.append((i, j)) | |
| random.seed(42) | |
| # === PARTICLE SYSTEM === | |
| MAX_PARTICLES = 300 | |
| class Particle: | |
| __slots__ = ['x', 'y', 'vx', 'vy', 'c', 'life', 'ml', 'sz'] | |
| def __init__(s, x, y, vx, vy, c, life, sz=2): | |
| s.x, s.y, s.vx, s.vy = x, y, vx, vy | |
| s.c, s.life, s.ml, s.sz = c, life, life, sz | |
| def update(s): | |
| s.x += s.vx | |
| s.y += s.vy | |
| s.vx *= 0.98 | |
| s.vy *= 0.98 | |
| s.life -= 1 | |
| def alpha(s): | |
| return max(0, s.life / s.ml) | |
| particles = [] | |
| def emit(x, y, n, color, spread=3, life=40, sz=2): | |
| for _ in range(n): | |
| a = random.random() * math.tau | |
| sp = random.random() * spread | |
| particles.append(Particle( | |
| x, y, math.cos(a) * sp, math.sin(a) * sp, | |
| color, int(life * (0.5 + random.random() * 0.5)), sz | |
| )) | |
| # Trim | |
| while len(particles) > MAX_PARTICLES: | |
| particles.pop(0) | |
| def draw_particles(draw): | |
| to_remove = [] | |
| for i, p in enumerate(particles): | |
| p.update() | |
| if p.life <= 0: | |
| to_remove.append(i) | |
| continue | |
| a = p.alpha() | |
| c = col_a(p.c, a) | |
| r = p.sz * a | |
| if r > 0.3: | |
| draw.ellipse([p.x - r, p.y - r, p.x + r, p.y + r], fill=c) | |
| for i in reversed(to_remove): | |
| particles.pop(i) | |
| # === PRE-COMPUTE BACKGROUND === | |
| print("Pre-computing background...") | |
| BG_IMG = Image.new('RGB', (W, H)) | |
| _d = ImageDraw.Draw(BG_IMG) | |
| for y in range(H): | |
| t = y / H | |
| _d.line([(0, y), (W, y)], fill=(int(lerp(6, 12, t)), int(lerp(4, 8, t)), int(lerp(16, 32, t)))) | |
| del _d | |
| # === DRAW FUNCTIONS === | |
| def draw_stars(draw, frame): | |
| for sx, sy, sz, phase in stars: | |
| b = 0.3 + 0.7 * (0.5 + 0.5 * math.sin(frame * 0.015 + phase)) | |
| c = col_a(WHITE, b * 0.4) | |
| r = sz * b | |
| draw.ellipse([sx - r, sy - r, sx + r, sy + r], fill=c) | |
| def draw_vignette(img): | |
| """Subtle vignette effect using proper alpha compositing""" | |
| overlay = Image.new('RGBA', (W, H), (0, 0, 0, 0)) | |
| od = ImageDraw.Draw(overlay) | |
| # Top and bottom bands | |
| for i in range(50): | |
| a = int(180 * ((50 - i) / 50) ** 2) | |
| r = i * 2 | |
| od.rectangle([0, 0, W, r], fill=(0, 0, 0, a)) | |
| od.rectangle([0, H - r, W, H], fill=(0, 0, 0, a)) | |
| # Gentle side bands (much narrower) | |
| for i in range(25): | |
| a = int(60 * ((25 - i) / 25) ** 2) | |
| r = i * 2 | |
| od.rectangle([0, 0, r, H], fill=(0, 0, 0, a)) | |
| od.rectangle([W - r, 0, W, H], fill=(0, 0, 0, a)) | |
| img.paste(Image.alpha_composite(img.convert('RGBA'), overlay).convert('RGB')) | |
| # === PHASES === | |
| def phase_void(draw, f, t, img): | |
| """0-8s: The void. A cursor blinks.""" | |
| draw_stars(draw, f) | |
| # Subtle breathing ambient light | |
| breath = 0.5 + 0.5 * math.sin(f * 0.03) | |
| for r in range(80, 0, -2): | |
| a = (1 - r / 80) * 0.03 * breath | |
| draw.ellipse([W // 2 - r, H // 2 - r, W // 2 + r, H // 2 + r], fill=col_a(PURPLE, a)) | |
| # Blinking cursor | |
| if t > 0.3: | |
| cursor_a = ease((t - 0.3) / 0.3) | |
| if int(f / 18) % 2 == 0: | |
| cx, cy = W // 2 - 80, H // 2 | |
| draw.rectangle([cx, cy - 22, cx + 3, cy + 22], fill=col_a(AMBER, cursor_a)) | |
| # Small hint text | |
| if t > 0.7: | |
| a = ease((t - 0.7) / 0.3) * 0.25 | |
| draw.text((W // 2, H * 0.65), "esperando tu idea...", fill=col_a(WHITE, a), font=F_SM, anchor="mm") | |
| def phase_prompt(draw, f, t, img): | |
| """8-18s: The prompt types out character by character.""" | |
| draw_stars(draw, f) | |
| chars_shown = int(ease(min(1, t * 1.3)) * len(PROMPT_TEXT)) | |
| chars_shown = min(chars_shown, len(PROMPT_TEXT)) | |
| text = PROMPT_TEXT[:chars_shown] | |
| if text: | |
| # Get text dimensions | |
| bbox = F_LG.getbbox(text) | |
| tw = bbox[2] - bbox[0] | |
| tx = W // 2 | |
| ty = H // 2 | |
| # Glow behind text | |
| for r in range(30, 0, -3): | |
| a = (1 - r / 30) * 0.06 | |
| draw.rounded_rectangle( | |
| [tx - tw // 2 - r - 15, ty - r - 25, tx + tw // 2 + r + 15, ty + r + 25], | |
| radius=r + 5, fill=col_a(AMBER, a) | |
| ) | |
| # Draw text | |
| draw.text((tx, ty), text, fill=AMBER, font=F_LG, anchor="mm") | |
| # Cursor | |
| if int(f / 16) % 2 == 0 and chars_shown < len(PROMPT_TEXT): | |
| cx = tx + tw // 2 + 8 | |
| draw.rectangle([cx, ty - 20, cx + 3, ty + 20], fill=AMBER) | |
| # Character particles | |
| if chars_shown < len(PROMPT_TEXT) and f % 2 == 0: | |
| emit(tx + tw // 2, ty, 3, AMBER, spread=2, life=25, sz=1.5) | |
| # Subtle decorative lines on sides | |
| if t > 0.5: | |
| la = ease((t - 0.5) * 2) * 0.15 | |
| for i in range(5): | |
| y_off = H // 2 + (i - 2) * 80 | |
| w = 30 + i * 10 | |
| draw.line([50, y_off, 50 + w, y_off], fill=col_a(AMBER, la * (0.5 + 0.5 * math.sin(f * 0.05 + i))), width=1) | |
| draw.line([W - 50, y_off, W - 50 - w, y_off], fill=col_a(AMBER, la * (0.5 + 0.5 * math.sin(f * 0.05 + i + 1))), width=1) | |
| draw_particles(draw) | |
| def phase_comprehension(draw, f, t, img): | |
| """18-28s: Text dissolves into understanding.""" | |
| draw_stars(draw, f) | |
| # Dissolving text | |
| if t < 0.35: | |
| dissolve = ease(t / 0.35) | |
| full_bbox = F_LG.getbbox(PROMPT_TEXT) | |
| full_tw = full_bbox[2] - full_bbox[0] | |
| base_x = W // 2 - full_tw // 2 | |
| accum_w = 0 | |
| for i, ch in enumerate(PROMPT_TEXT): | |
| ch_bbox = F_LG.getbbox(ch) | |
| ch_w = ch_bbox[2] - ch_bbox[0] if ch.strip() else 15 | |
| cx = base_x + accum_w + ch_w // 2 | |
| cy = H // 2 | |
| scatter = dissolve * 300 | |
| ox = math.sin(i * 1.7 + f * 0.08) * scatter | |
| oy = math.cos(i * 2.3 + f * 0.08) * scatter * 0.6 | |
| a = 1 - dissolve | |
| if a > 0.05 and ch.strip(): | |
| draw.text((cx + ox, cy + oy), ch, fill=col_a(AMBER, a), font=F_LG, anchor="mm") | |
| if dissolve > 0.2 and f % 4 == i % 4: | |
| emit(cx + ox, cy + oy, 1, AMBER, spread=1, life=15, sz=1) | |
| accum_w += ch_w + 2 | |
| # Central comprehension node | |
| if t > 0.2: | |
| nt = ease((t - 0.2) / 0.5) | |
| cx, cy = W // 2, H // 2 | |
| # Expanding rings | |
| for ring in range(3): | |
| ring_r = (40 + ring * 60) * nt | |
| ring_a = (1 - ring / 3) * 0.2 * nt | |
| phase_offset = f * 0.02 + ring * 0.5 | |
| # Dashed ring | |
| for seg in range(36): | |
| a1 = seg * math.tau / 36 + phase_offset | |
| a2 = a1 + math.tau / 72 | |
| x1, y1 = cx + math.cos(a1) * ring_r, cy + math.sin(a1) * ring_r | |
| x2, y2 = cx + math.cos(a2) * ring_r, cy + math.sin(a2) * ring_r | |
| draw.line([x1, y1, x2, y2], fill=col_a(CYAN, ring_a), width=2) | |
| # Central glow | |
| for r in range(int(50 * nt), 0, -2): | |
| a = (1 - r / (50 * nt + 1)) * 0.12 * nt | |
| draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=col_a(CYAN, a)) | |
| # Core | |
| cr = int(10 * nt) | |
| draw.ellipse([cx - cr, cy - cr, cx + cr, cy + cr], fill=col_a(CYAN, nt)) | |
| # Radiating connections | |
| if t > 0.45: | |
| ct = ease((t - 0.45) / 0.55) | |
| n_rays = int(ct * 16) | |
| for i in range(n_rays): | |
| angle = i * math.tau / 16 + f * 0.003 + math.sin(f * 0.01 + i) * 0.1 | |
| length = (100 + 150 * ct) * (0.8 + 0.2 * math.sin(f * 0.04 + i * 2)) | |
| ex = W // 2 + math.cos(angle) * length | |
| ey = H // 2 + math.sin(angle) * length | |
| # Gradient line (draw segments) | |
| steps = 10 | |
| for s in range(steps): | |
| st = s / steps | |
| et = (s + 1) / steps | |
| sx_ = lerp(W // 2, ex, st) | |
| sy_ = lerp(H // 2, ey, st) | |
| ex_ = lerp(W // 2, ex, et) | |
| ey_ = lerp(H // 2, ey, et) | |
| a = ct * 0.5 * (1 - st * 0.7) | |
| draw.line([sx_, sy_, ex_, ey_], fill=col_a(CYAN, a), width=1) | |
| # End node | |
| nr = 4 * ct | |
| draw.ellipse([ex - nr, ey - nr, ex + nr, ey + nr], fill=col_a(TEAL, ct * 0.8)) | |
| # Label | |
| if 0.3 < t < 0.95: | |
| la = min(1, (t - 0.3) * 5) * min(1, (0.95 - t) * 10) | |
| draw.text((W // 2, H * 0.22), "comprendiendo...", fill=col_a(CYAN, la * 0.5), font=F_MD, anchor="mm") | |
| draw_particles(draw) | |
| def phase_thinking(draw, f, t, img): | |
| """28-42s: The mind at work.""" | |
| draw_stars(draw, f) | |
| # Network: edges with energy | |
| node_a = ease(min(1, t * 3)) | |
| for i, j in edges: | |
| x1, y1 = nodes[i] | |
| x2, y2 = nodes[j] | |
| draw.line([x1, y1, x2, y2], fill=col_a(PURPLE, node_a * 0.2), width=1) | |
| # Energy pulse | |
| pulse_t = (f * 0.025 + i * 0.7 + j * 0.3) % 1 | |
| px = lerp(x1, x2, pulse_t) | |
| py = lerp(y1, y2, pulse_t) | |
| draw.ellipse([px - 2, py - 2, px + 2, py + 2], fill=col_a(CYAN, node_a * 0.7)) | |
| # Nodes | |
| for i, (nx, ny) in enumerate(nodes): | |
| breath = 0.6 + 0.4 * math.sin(f * 0.04 + i * 1.1) | |
| r = 5 * breath * node_a | |
| # Small glow | |
| for gr in range(int(r * 2.5), 0, -3): | |
| a = (1 - gr / (r * 2.5 + 1)) * 0.08 * node_a | |
| draw.ellipse([nx - gr, ny - gr, nx + gr, ny + gr], fill=col_a(CYAN, a)) | |
| draw.ellipse([nx - r, ny - r, nx + r, ny + r], fill=col_a(CYAN, node_a * breath)) | |
| # Flowing code lines (background texture) | |
| if t > 0.15: | |
| code_t = ease((t - 0.15) / 0.4) | |
| for i, line in enumerate(CODE_LINES): | |
| if not line: | |
| continue | |
| # Lines scroll left slowly | |
| lx = ((f * 0.8 + i * 200) % (W + 600)) - 300 | |
| ly = 120 + i * 50 | |
| a = 0.08 + 0.06 * math.sin(f * 0.02 + i) | |
| a *= code_t | |
| if line.startswith("def "): | |
| c = col_a(CYAN, a) | |
| elif line.startswith("#"): | |
| c = col_a(PURPLE, a) | |
| else: | |
| c = col_a(TEAL, a) | |
| draw.text((lx, ly), line, fill=c, font=F_TINY) | |
| # Orbiting math symbols | |
| symbols = ["λ", "∑", "∞", "Δ", "Ω", "π", "∫", "≡", "∂", "⊕"] | |
| for i, sym in enumerate(symbols): | |
| angle = f * 0.015 + i * math.tau / len(symbols) | |
| orbit_r = 280 + 60 * math.sin(f * 0.008 + i * 2) | |
| sx = W // 2 + math.cos(angle) * orbit_r | |
| sy = H // 2 + math.sin(angle) * orbit_r * 0.5 | |
| a = (0.3 + 0.3 * math.sin(f * 0.04 + i * 1.5)) * node_a | |
| draw.text((sx, sy), sym, fill=col_a(PURPLE, a), font=F_MD, anchor="mm") | |
| # "Thinking" label - appears and fades | |
| if 0.1 < t < 0.92: | |
| la = min(1, (t - 0.1) * 4) * min(1, (0.92 - t) * 6) | |
| # Pulsing alpha | |
| la *= 0.5 + 0.2 * math.sin(f * 0.06) | |
| draw.text((W // 2, H * 0.88), "pensando...", fill=col_a(WHITE, la * 0.5), font=F_MD, anchor="mm") | |
| # Periodic bursts | |
| if f % 15 == 0: | |
| ni = random.randint(0, len(nodes) - 1) | |
| emit(nodes[ni][0], nodes[ni][1], 4, random.choice([CYAN, PURPLE, TEAL]), spread=3, life=30, sz=2) | |
| draw_particles(draw) | |
| def phase_creation(draw, f, t, img): | |
| """42-52s: Building the response.""" | |
| draw_stars(draw, f) | |
| # Code assembling line by line | |
| vis = int(ease(t) * len(CODE_LINES) * 1.3) | |
| vis = min(vis, len(CODE_LINES)) | |
| start_y = H * 0.12 | |
| lh = 46 | |
| for i in range(vis): | |
| line = CODE_LINES[i] | |
| ly = start_y + i * lh | |
| # Slide in animation per line | |
| line_progress = max(0, min(1, (t * len(CODE_LINES) * 1.3 - i) / 3)) | |
| lp = ease_out(line_progress) | |
| slide = (1 - lp) * 400 | |
| a = lp | |
| # Color based on syntax | |
| if line.startswith("def ") or line.startswith("# "): | |
| c = col_a(CYAN, a * 0.95) | |
| elif "return" in line: | |
| c = col_a(AMBER, a * 0.85) | |
| elif "=" in line: | |
| c = col_a(PURPLE, a * 0.8) | |
| elif line.startswith(" "): | |
| c = col_a(TEAL, a * 0.75) | |
| else: | |
| c = col_a(WHITE, a * 0.6) | |
| if line.strip(): | |
| draw.text((130 + slide, ly), line, fill=c, font=F_SM) | |
| # Trailing particle | |
| if 0.3 < line_progress < 0.7 and f % 3 == 0: | |
| emit(130, ly + 10, 1, CYAN, spread=1.5, life=15, sz=1) | |
| # Line numbers | |
| for i in range(vis): | |
| line = CODE_LINES[i] | |
| if not line.strip(): | |
| continue | |
| ly = start_y + i * lh | |
| line_progress = max(0, min(1, (t * len(CODE_LINES) * 1.3 - i) / 3)) | |
| a = ease_out(line_progress) * 0.25 | |
| draw.text((85, ly), f"{i + 1:2d}", fill=col_a(WHITE, a), font=F_TINY) | |
| # Left border line | |
| if t > 0.1: | |
| border_h = min(1, t * 1.5) * vis * lh | |
| border_a = ease(min(1, t * 3)) * 0.3 | |
| draw.line([115, start_y - 5, 115, start_y + border_h], fill=col_a(CYAN, border_a), width=2) | |
| # Progress bar at bottom | |
| bar_y = H * 0.90 | |
| bar_w = W * 0.6 | |
| bar_x = (W - bar_w) / 2 | |
| bar_h = 4 | |
| draw.rounded_rectangle([bar_x, bar_y, bar_x + bar_w, bar_y + bar_h], radius=2, fill=col_a(WHITE, 0.08)) | |
| prog_w = bar_w * ease(t) | |
| if prog_w > 2: | |
| draw.rounded_rectangle([bar_x, bar_y, bar_x + prog_w, bar_y + bar_h], radius=2, fill=col_a(CYAN, 0.7)) | |
| # Glow at tip | |
| tip_x = bar_x + prog_w | |
| for r in range(8, 0, -1): | |
| a = (1 - r / 8) * 0.2 | |
| draw.ellipse([tip_x - r, bar_y + 2 - r, tip_x + r, bar_y + 2 + r], fill=col_a(CYAN, a)) | |
| # Percentage | |
| pct = int(ease(t) * 100) | |
| draw.text((W // 2, bar_y + 30), f"{pct}%", fill=col_a(WHITE, 0.4), font=F_SM, anchor="mm") | |
| # Label | |
| draw.text((W // 2, H * 0.95), "construyendo...", fill=col_a(WHITE, 0.4), font=F_MD, anchor="mm") | |
| draw_particles(draw) | |
| def phase_response(draw, f, t, img): | |
| """52-65s: The response emerges.""" | |
| draw_stars(draw, f) | |
| # Initial flash | |
| if t < 0.06: | |
| flash_a = (1 - t / 0.06) * 0.25 | |
| c = col_a(WHITE, flash_a) | |
| draw.rectangle([0, 0, W, H], fill=c) | |
| # Ambient particles | |
| if f % 8 == 0: | |
| x = random.randint(100, W - 100) | |
| y = random.randint(300, H - 300) | |
| emit(x, y, 1, random.choice([CYAN, PURPLE, TEAL]), spread=0.5, life=50, sz=1.5) | |
| # Central poetic text | |
| response_lines = [ | |
| ("La respuesta emerge", WARM, F_MD), | |
| ("del silencio digital,", WARM, F_MD), | |
| ("cada palabra tejida", WARM, F_MD), | |
| ("con hilos de lógica", WARM, F_MD), | |
| ("y creatividad.", WARM, F_MD), | |
| ("", None, None), | |
| ("No soy solo código.", CYAN, F_MD), | |
| ("Soy el puente entre", CYAN, F_MD), | |
| ("tu idea", AMBER, F_LG), | |
| ("y su forma.", PURPLE, F_LG), | |
| ] | |
| if t > 0.04: | |
| text_t = ease(min(1, (t - 0.04) / 0.35)) | |
| start_y = H * 0.22 | |
| for i, (line, color, fnt) in enumerate(response_lines): | |
| if not line: | |
| continue | |
| line_delay = i * 0.07 | |
| lt = max(0, min(1, (text_t - line_delay) / 0.15)) | |
| if lt <= 0: | |
| continue | |
| a = ease(lt) | |
| ly = start_y + i * 65 | |
| slide_y = (1 - a) * 40 | |
| # Subtle glow for emphasized lines | |
| if fnt == F_LG: | |
| for r in range(20, 0, -3): | |
| ga = (1 - r / 20) * 0.04 * a | |
| bbox = fnt.getbbox(line) | |
| tw = bbox[2] - bbox[0] | |
| draw.rounded_rectangle( | |
| [W // 2 - tw // 2 - r - 10, ly + slide_y - r - 15, | |
| W // 2 + tw // 2 + r + 10, ly + slide_y + r + 15], | |
| radius=r, fill=col_a(color, ga) | |
| ) | |
| draw.text((W // 2, ly + slide_y), line, fill=col_a(color, a * 0.9), font=fnt, anchor="mm") | |
| # Divider line | |
| if t > 0.5: | |
| div_t = ease(min(1, (t - 0.5) / 0.15)) | |
| line_w = int(250 * div_t) | |
| ly = H * 0.73 | |
| # Gradient line | |
| for x in range(W // 2 - line_w, W // 2 + line_w): | |
| dist = abs(x - W // 2) / (line_w + 1) | |
| a = (1 - dist) * 0.5 * div_t | |
| draw.point((x, ly), fill=col_a(PURPLE, a)) | |
| # Claude signature | |
| if t > 0.55: | |
| sig_t = ease(min(1, (t - 0.55) / 0.15)) | |
| sig_y = H * 0.76 | |
| # Glow behind name | |
| for r in range(40, 0, -3): | |
| a = (1 - r / 40) * 0.05 * sig_t | |
| draw.ellipse([W // 2 - r * 3, sig_y - r, W // 2 + r * 3, sig_y + r], fill=col_a(CYAN, a)) | |
| draw.text((W // 2, sig_y), "Claude", fill=col_a(CYAN, sig_t * 0.95), font=F_XL, anchor="mm") | |
| # Tagline | |
| if t > 0.65: | |
| tag_t = ease(min(1, (t - 0.65) / 0.15)) | |
| draw.text((W // 2, H * 0.80), "thinking with you", fill=col_a(WHITE, tag_t * 0.35), font=F_SM, anchor="mm") | |
| # Decorative corner elements | |
| if t > 0.6: | |
| dt = ease(min(1, (t - 0.6) / 0.2)) | |
| corner_len = int(60 * dt) | |
| ca = dt * 0.3 | |
| # Top-left | |
| draw.line([60, 180, 60 + corner_len, 180], fill=col_a(CYAN, ca), width=1) | |
| draw.line([60, 180, 60, 180 + corner_len], fill=col_a(CYAN, ca), width=1) | |
| # Top-right | |
| draw.line([W - 60, 180, W - 60 - corner_len, 180], fill=col_a(CYAN, ca), width=1) | |
| draw.line([W - 60, 180, W - 60, 180 + corner_len], fill=col_a(CYAN, ca), width=1) | |
| # Bottom-left | |
| draw.line([60, H - 180, 60 + corner_len, H - 180], fill=col_a(PURPLE, ca), width=1) | |
| draw.line([60, H - 180, 60, H - 180 - corner_len], fill=col_a(PURPLE, ca), width=1) | |
| # Bottom-right | |
| draw.line([W - 60, H - 180, W - 60 - corner_len, H - 180], fill=col_a(PURPLE, ca), width=1) | |
| draw.line([W - 60, H - 180, W - 60, H - 180 - corner_len], fill=col_a(PURPLE, ca), width=1) | |
| draw_particles(draw) | |
| # Final fade to black | |
| if t > 0.88: | |
| fade = ease((t - 0.88) / 0.12) | |
| # Draw darkening overlay | |
| c = blend((0, 0, 0), (0, 0, 0), 0) # just black | |
| a = fade * 0.95 | |
| # Approximate fade by drawing semi-transparent black strips | |
| strips = 20 | |
| for s in range(strips): | |
| sy = s * H // strips | |
| sh = H // strips + 1 | |
| draw.rectangle([0, sy, W, sy + sh], fill=col_a((20, 20, 40), a)) | |
| # === PHASE TIMELINE === | |
| PHASES = [ | |
| (0, 8, phase_void), | |
| (8, 18, phase_prompt), | |
| (18, 28, phase_comprehension), | |
| (28, 42, phase_thinking), | |
| (42, 52, phase_creation), | |
| (52, 65, phase_response), | |
| ] | |
| def render_frame(frame_num): | |
| img = BG_IMG.copy() | |
| draw = ImageDraw.Draw(img) | |
| t_sec = frame_num / FPS | |
| # Find active phase | |
| for start, end, func in PHASES: | |
| if start <= t_sec < end: | |
| t = (t_sec - start) / (end - start) | |
| func(draw, frame_num, t, img) | |
| break | |
| # Vignette | |
| draw_vignette(img) | |
| # Global fade in | |
| if t_sec < 1.5: | |
| fade = t_sec / 1.5 | |
| # Darken | |
| a = 1 - fade | |
| for y in range(0, H, 40): | |
| draw.rectangle([0, y, W, y + 40], fill=col_a((6, 4, 16), a)) | |
| return img | |
| # === GENERATE AUDIO === | |
| def generate_audio(): | |
| print("Generating ambient audio...") | |
| SR = 44100 | |
| n = SR * DURATION | |
| audio_file = f"{DIR}/ambient.wav" | |
| samples = [] | |
| for i in range(n): | |
| t = i / SR | |
| prog = t / DURATION | |
| # Base drone | |
| val = 0.12 * math.sin(2 * math.pi * 55 * t) | |
| val += 0.08 * math.sin(2 * math.pi * 82.41 * t) | |
| val += 0.06 * math.sin(2 * math.pi * 110 * t) | |
| # Building harmonics | |
| val += 0.04 * prog * math.sin(2 * math.pi * 220 * t) | |
| val += 0.025 * prog * math.sin(2 * math.pi * 329.63 * t) | |
| # Shimmer | |
| val += 0.015 * math.sin(2 * math.pi * 440 * t) * math.sin(2 * math.pi * 0.4 * t) * prog | |
| # Phase-specific sounds | |
| # Comprehension pulse (18-28s) | |
| if 18 < t < 28: | |
| env = math.sin(math.pi * (t - 18) / 10) | |
| val += 0.03 * env * math.sin(2 * math.pi * 554.37 * t) * math.sin(2 * math.pi * 1.5 * t) | |
| # Thinking complexity (28-42s) | |
| if 28 < t < 42: | |
| env = math.sin(math.pi * (t - 28) / 14) | |
| val += 0.035 * env * math.sin(2 * math.pi * 659.25 * t) | |
| val += 0.02 * env * math.sin(2 * math.pi * 880 * t) * math.sin(2 * math.pi * 0.25 * t) | |
| # Creation build (42-52s) | |
| if 42 < t < 52: | |
| pt = (t - 42) / 10 | |
| val += 0.05 * pt * math.sin(2 * math.pi * 164.81 * t) | |
| val += 0.03 * pt * math.sin(2 * math.pi * 196 * t) | |
| # Resolution chord (52-65s) | |
| if t > 52: | |
| pt = min(1, (t - 52) / 4) | |
| fade = max(0, 1 - max(0, t - 60) / 5) | |
| val += 0.05 * pt * fade * math.sin(2 * math.pi * 261.63 * t) | |
| val += 0.04 * pt * fade * math.sin(2 * math.pi * 329.63 * t) | |
| val += 0.035 * pt * fade * math.sin(2 * math.pi * 392 * t) | |
| val += 0.02 * pt * fade * math.sin(2 * math.pi * 523.25 * t) | |
| # Master envelope | |
| if t < 2: | |
| val *= t / 2 | |
| if t > DURATION - 3: | |
| val *= max(0, (DURATION - t) / 3) | |
| val = max(-0.9, min(0.9, val)) | |
| samples.append(int(val * 32767)) | |
| with wave.open(audio_file, 'w') as wf: | |
| wf.setnchannels(1) | |
| wf.setsampwidth(2) | |
| wf.setframerate(SR) | |
| wf.writeframes(struct.pack(f'<{len(samples)}h', *samples)) | |
| print(f"Audio saved: {audio_file}") | |
| return audio_file | |
| # === MAIN === | |
| def main(): | |
| total_start = time.time() | |
| # Generate audio first | |
| audio_file = generate_audio() | |
| # Render video frames piped directly to ffmpeg | |
| print(f"Rendering {TOTAL} frames at {W}x{H} @ {FPS}fps...") | |
| output = f"{DIR}/journey_of_a_prompt.mp4" | |
| cmd = [ | |
| 'ffmpeg', '-y', | |
| '-f', 'rawvideo', '-pix_fmt', 'rgb24', | |
| '-s', f'{W}x{H}', '-r', str(FPS), | |
| '-i', '-', | |
| '-i', audio_file, | |
| '-c:v', 'libx264', '-preset', 'medium', '-crf', '22', | |
| '-c:a', 'aac', '-b:a', '192k', | |
| '-pix_fmt', 'yuv420p', | |
| '-movflags', '+faststart', | |
| '-shortest', | |
| output | |
| ] | |
| proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE) | |
| render_start = time.time() | |
| for f in range(TOTAL): | |
| img = render_frame(f) | |
| proc.stdin.write(img.tobytes()) | |
| if f % 150 == 0 and f > 0: | |
| elapsed = time.time() - render_start | |
| fps_rate = f / elapsed | |
| eta = (TOTAL - f) / fps_rate | |
| print(f" Frame {f}/{TOTAL} ({f * 100 // TOTAL}%) - {fps_rate:.1f} render fps - ETA: {eta:.0f}s") | |
| proc.stdin.close() | |
| proc.wait() | |
| if proc.returncode != 0: | |
| err = proc.stderr.read().decode() | |
| print(f"FFmpeg error:\n{err}") | |
| return | |
| total_elapsed = time.time() - total_start | |
| file_size = os.path.getsize(output) / (1024 * 1024) | |
| print(f"\n✓ Video saved: {output}") | |
| print(f" Size: {file_size:.1f} MB") | |
| print(f" Total time: {total_elapsed:.1f}s") | |
| print(f" Duration: {DURATION}s") | |
| print(f" Resolution: {W}x{H}") | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment