#!/usr/bin/env python3 """Inside the Loop: original procedural motion design and music, encoded by FFmpeg.""" from __future__ import annotations import argparse from functools import lru_cache import json import math import os from pathlib import Path import subprocess import time import wave import numpy as np from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont ROOT = Path(__file__).resolve().parent OUT = ROOT / "output" W, H, FPS = 1080, 1920, 30 BPM, BEAT, DURATION = 144, 60 / 144, 30 INK = (9, 14, 21) PAPER = (240, 238, 226) LIME = (218, 255, 93) BLUE = (70, 75, 246) ORANGE = (255, 111, 69) GRAY = (123, 142, 153) SCENES = [0, 4, 12, 20, 28, 36, 44, 52, 60, 68, 72] NAMES = ["INPUT", "ACTIVATE", "CONNECT", "EXPLORE", "DEBUG", "CREATE", "VERIFY", "REFINE", "COLLABORATE", "BUILD"] FFMPEG = str(ROOT / ".ffmpeg-bin/bin/ffmpeg") if (ROOT / ".ffmpeg-bin/bin/ffmpeg").exists() else "ffmpeg" def clamp(x, a=0., b=1.): return min(b, max(a, x)) def ease(x): return 1 - (1 - clamp(x)) ** 3 def smooth(x): x = clamp(x) return x * x * (3 - 2 * x) def mix(a, b, t): return tuple(round(x * (1 - t) + y * t) for x, y in zip(a, b)) @lru_cache(maxsize=160) def font(size, kind="bold"): name = {"bold": "Inter-Black.ttf", "medium": "Inter-Medium.ttf", "mono": "DejaVuSansMono.ttf"}[kind] return ImageFont.truetype(str(ROOT / "assets" / name), int(size)) @lru_cache(maxsize=512) def text_asset(text, size, color, kind="bold", outline=False): f = font(size, kind) box = f.getbbox(text, stroke_width=2 if outline else 0) im = Image.new("RGBA", (box[2] - box[0] + 8, box[3] - box[1] + 8)) d = ImageDraw.Draw(im) d.text((4 - box[0], 4 - box[1]), text, font=f, fill=(0, 0, 0, 0) if outline else color, stroke_width=2 if outline else 0, stroke_fill=color) return im def label(im, text, x, y, size=26, color=GRAY, kind="mono", align="left"): tile = text_asset(text, size, color, kind) if align == "center": x -= tile.width / 2 elif align == "right": x -= tile.width im.paste(tile, (int(x), int(y)), tile) def headline(im, lines, top, size, color=PAPER, accent=None, age=10, gap=20, left=76, max_width=930, stagger=.105): y = top for i, line in enumerate(lines): col = accent if accent and i == len(lines) - 1 else color tile = text_asset(line, size, col) if tile.width > max_width: tile = tile.resize((max_width, tile.height), Image.Resampling.LANCZOS) p = ease((age - i * stagger) / .42) if p > 0: visible = tile.copy() visible.putalpha(visible.getchannel("A").point(lambda a: int(a * p))) im.paste(visible, (int(left - (1 - p) * 140), int(y + (1 - p) * 54)), visible) y += tile.height + gap return y @lru_cache(maxsize=10) def background(style): palettes = { "dark": (INK, (24, 35, 32)), "paper": (PAPER, (228, 229, 216)), "blue": (BLUE, (45, 34, 171)), "orange": (ORANGE, (228, 78, 48)), "lime": (LIME, (188, 235, 73)), } base, glow = palettes[style] yy, xx = np.mgrid[0:H, 0:W].astype(np.float32) halo = np.exp(-(((xx - 740) / 720) ** 2 + ((yy - 1180) / 800) ** 2)) grain = np.random.default_rng(808).normal(0, .65, (H, W)) arr = np.zeros((H, W, 3), dtype=np.uint8) for ch in range(3): arr[:, :, ch] = np.clip(base[ch] + (glow[ch] - base[ch]) * halo + grain, 0, 255) return Image.fromarray(arr) def grid(im, color, step=108, offset=0): d = ImageDraw.Draw(im) for x in range(0, W, step): d.line((x, 0, x, H), fill=color, width=1) for y in range(-step, H + step, step): d.line((0, y + int(offset) % step, W, y + int(offset) % step), fill=color, width=1) def cross(d, x, y, color, r=9, width=2): d.line((x - r, y, x + r, y), fill=color, width=width) d.line((x, y - r, x, y + r), fill=color, width=width) def chrome(im, idx, t, dark=False): col = INK if dark else PAPER muted = mix(col, LIME if dark and idx == 8 else (PAPER if dark else INK), .45) d = ImageDraw.Draw(im) d.line((76, 138, 1004, 138), fill=muted, width=2) label(im, "CODEX / POV", 76, 87, 27, col) d.ellipse((975, 93, 992, 110), fill=col) label(im, f"{idx + 1:02d} / {NAMES[idx]}", 76, 168, 24, muted) # A transport-style footer stays inside the central safe area of a phone screen. label(im, "INSIDE THE LOOP", 76, 1706, 24, col) label(im, f"00:{min(29, int(t)):02d}", 1004, 1706, 24, col, align="right") for j in range(72): x = 78 + j * 12.95 d.line((x, 1770, x, 1784 if j % 4 else 1795), fill=col if j <= t / BEAT else muted, width=4 if j <= t / BEAT else 1) def project(points, t, center=(540, 1220), scale=340, rot_speed=.45): p = np.asarray(points, dtype=float) a = t * rot_speed b = .55 + .25 * math.sin(t * .38) ry = np.array([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) rx = np.array([[1, 0, 0], [0, math.cos(b), -math.sin(b)], [0, math.sin(b), math.cos(b)]]) p = p @ ry.T @ rx.T factor = 4.6 / (4.6 + p[:, 2]) screen = np.column_stack((center[0] + p[:, 0] * scale * factor, center[1] + p[:, 1] * scale * factor)) return screen, p[:, 2] def add_glow(im, layer, strength=.6, blur=12): small = layer.resize((W // 4, H // 4), Image.Resampling.BILINEAR).filter(ImageFilter.GaussianBlur(blur)) halo = small.resize((W, H), Image.Resampling.BILINEAR) if strength < 1: halo = Image.blend(Image.new("RGB", (W, H)), halo, strength) return ImageChops.add(ImageChops.add(im, halo), layer) def torus(im, t, center=(540, 1260), scale=315, color=LIME, glow=True): layer = Image.new("RGB", (W, H)) d = ImageDraw.Draw(layer) beat = math.exp(-((t / BEAT) % 1) * 7) scale *= 1 + .025 * beat # A torus knot: three interwoven ribbons, no stock assets or logo tracing. for strand in range(24): u = np.linspace(0, math.tau, 190) v = strand * math.tau / 24 + t * .17 radius = 1.0 + .32 * np.cos(3 * u + v) p = np.column_stack((radius * np.cos(2 * u), radius * np.sin(2 * u), .32 * np.sin(3 * u + v))) q, z = project(p, t, center, scale) for j in range(len(q) - 1): brightness = .28 + .7 * clamp((z[j] + 1.1) / 2.2) c = tuple(int(x * brightness) for x in color) d.line([tuple(q[j]), tuple(q[j + 1])], fill=c, width=2) for k in range(7): u = t * .7 + k * math.tau / 7 p = [[(1 + .32 * math.cos(3*u)) * math.cos(2*u), (1 + .32 * math.cos(3*u)) * math.sin(2*u), .32 * math.sin(3*u)]] q, _ = project(p, t, center, scale) x, y = q[0] d.ellipse((x-5, y-5, x+5, y+5), fill=PAPER) return add_glow(im, layer, .7) if glow else ImageChops.add(im, layer) def orbit_lines(im, t, center, radius, color, n=4): d = ImageDraw.Draw(im) for k in range(n): rr = radius + k * 23 a = math.degrees(t * .7 + k * 1.7) d.arc((center[0]-rr, center[1]-rr, center[0]+rr, center[1]+rr), a, a+230, fill=color, width=2) RNG = np.random.default_rng(33) POINTS = RNG.normal(size=(36, 3)) POINTS /= np.linalg.norm(POINTS, axis=1)[:, None] EDGES = [(i,j) for i in range(len(POINTS)) for j in range(i) if np.linalg.norm(POINTS[i]-POINTS[j]) < .75] def network(im, t, age): d = ImageDraw.Draw(im) q, z = project(POINTS, t, (540, 1250), 365, .28) active = int(age * 13) for i, j in EDGES: d.line([tuple(q[i]), tuple(q[j])], fill=(169, 179, 165), width=2) for k, (i,j) in enumerate(EDGES[:max(0, active)]): p = ((age * .8 + k * .14) % 1) x,y = q[i] * (1-p) + q[j] * p d.ellipse((x-4,y-4,x+4,y+4), fill=BLUE) for i,(x,y) in enumerate(q): r = 6 if i % 4 else 11 d.ellipse((x-r, y-r, x+r, y+r), fill=BLUE if i < active else INK) for txt, idx in [("CONTEXT",3),("IDEAS",11),("CONSTRAINTS",18)]: x,y=q[idx] x,y=clamp(x,130,800),clamp(y,990,1510) tile=text_asset(txt,22, PAPER,"mono") d.rectangle((x-8,y-45,x+tile.width+8,y-9),fill=INK) im.paste(tile,(int(x),int(y-40)),tile) def terminal(im, x, y, width, height, rows, age, active=0, accent=LIME): d = ImageDraw.Draw(im) d.rounded_rectangle((x+12,y+15,x+width+12,y+height+15), radius=18, fill=(0,0,0)) d.rounded_rectangle((x,y,x+width,y+height), radius=18, fill=(15,23,30), outline=(63,77,82), width=2) for k,col in enumerate([ORANGE,LIME,GRAY]): d.ellipse((x+27+k*25,y+26,x+37+k*25,y+36),fill=col) label(im,"workspace / live",x+width-24,y+23,20,GRAY,align="right") d.line((x,y+67,x+width,y+67),fill=(52,65,72),width=2) for k,row in enumerate(rows): start=k*.12 if age < start: continue line=row[0] color=row[1] if len(row)>1 else PAPER n=min(len(line), max(0,int((age-start)*70))) yy=y+98+k*53 if k==active: d.rectangle((x+14,yy-8,x+width-15,yy+40),fill=(33,48,44)) label(im,line[:n],x+28,yy,27,color) def scene(idx, t, age): style = ["dark","dark","paper","dark","orange","blue","paper","dark","lime","dark"][idx] im = background(style).copy() dark = style in ("paper","orange","lime") d = ImageDraw.Draw(im) if idx == 0: grid(im,(20,30,36),108,t*20) label(im,"A DAY IN THE PROCESS",76,325,25,GRAY) headline(im,["YOU","TYPE."],475,222,age=age,accent=LIME) p=ease(age/.9) d.rectangle((78,1100,1001,1282),outline=(67,82,79),width=2) label(im,">",109,1157,55,LIME) message="let's make something" typed=message[:int(clamp(age/.95)*len(message))] label(im,typed,182,1170,37,PAPER) if int(t*7)%2==0: xx=190+font(37,"mono").getlength(typed) d.rectangle((xx,1169,xx+15,1210),fill=LIME) label(im,"ONE PROMPT. SOMETHING STARTS.",78,1380,25,GRAY) d.rectangle((78,1460,78+int(924*p),1466),fill=LIME) elif idx == 1: grid(im,(21,30,35),108,t*14) im=torus(im,t,scale=250) orbit_lines(im,t,(540,1260),340,(55,72,65),2) headline(im,["I GO","TO WORK."],350,202,age=age,accent=LIME) label(im,"CURIOSITY, IN MOTION.",78,845,27,PAPER) label(im,"READ / REASON / MAKE",540,1640,23,GRAY,align="center") elif idx == 2: grid(im,(216,217,204),108) network(im,t,age) headline(im,["CONNECT","THE","DOTS."],340,179,INK,BLUE,age) label(im,"EVERY DETAIL CAN CHANGE THE PICTURE.",78,889,24,INK) label(im,"PATTERNS INTO POSSIBILITIES",540,1610,24,INK,align="center") elif idx == 3: # Perspective corridor of files; speed lines converge underneath the title. cx,cy=540,1140 for j in range(16): z=(j/16+t*.21)%1 sc=.1+z*z*2.7 ww,hh=480*sc,255*sc c=mix(INK,(74,100,91),z*.8) d.rectangle((cx-ww,cy-hh,cx+ww,cy+hh),outline=c,width=2) for x in [-450,-100,240,840,1180,1530]: d.line((cx,cy,x,H),fill=(39,64,56),width=2) terminal(im,97,1020,886,481,[ ("$ rg 'what matters'",LIME), (" context.ts 12 matches",GRAY), (" ideas.md 04 threads",GRAY), (" the-next-step found",PAPER), ("$ follow the signal_",LIME), ],age,active=3) headline(im,["FOLLOW","THE","SIGNAL."],333,186,age=age,accent=LIME) label(im,"READ DEEP. FIND A WAY THROUGH.",78,1595,26,PAPER) elif idx == 4: grid(im,(229,89,56),108,t*38) headline(im,["HUNT","THE","BUG."],330,229,INK,INK,age) fixed=age>1.68 rows=[ ("- return items[index + 1]",ORANGE), ("+ return items[index]",LIME), ("",GRAY), (("PASS boundary case" if fixed else "FAIL boundary case"),LIME if fixed else ORANGE), ] terminal(im,97,1110,886,372,rows,age,active=3) cx,cy=875,996 d=ImageDraw.Draw(im) d.ellipse((cx-88,cy-88,cx+88,cy+88),outline=INK,width=9) cross(d,cx,cy,INK,119,4) if fixed: d.line((cx-37,cy,cx-6,cy+27,cx+43,cy-32),fill=INK,width=11) else: d.line((cx-28,cy-28,cx+28,cy+28),fill=INK,width=9) d.line((cx-28,cy+28,cx+28,cy-28),fill=INK,width=9) label(im,"TRACE IT. PATCH IT. PROVE IT.",78,1588,26,INK) elif idx == 5: grid(im,(83,89,249),108,t*16) # An interface assembles in perspective from wireframe layers. d=ImageDraw.Draw(im) for j in range(4): p=ease((age-j*.2)/1.0) x=144+j*27 y=1170-j*38+(1-p)*280 quad=[(x,y),(x+630,y-116),(x+760,y+224),(x+130,y+341)] d.polygon(quad,fill=mix(BLUE,INK,.22+j*.1),outline=mix(BLUE,PAPER,.35+j*.15),width=3) if j==3: for line in range(5): yy=y+18+line*40 d.line((x+58,yy,x+390+(line%2)*120,yy-64),fill=PAPER if line<2 else (146,149,254),width=10 if line==0 else 5) d.polygon([(x+400,y+125),(x+570,y+94),(x+598,y+166),(x+428,y+197)],fill=LIME) headline(im,["MAKE","IT","REAL."],322,225,PAPER,LIME,age) label(im,"FROM A BLANK FILE TO A FIRST VERSION.",78,1585,24,PAPER) elif idx == 6: headline(im,["RUN IT.","BREAK IT.","FIX IT."],344,166,INK,BLUE,age,gap=40) d=ImageDraw.Draw(im) for j,title in enumerate(["EDGE CASES","THE HAPPY PATH","ONE MORE CHECK"]): yy=1100+j*139 p=ease((age-j*.53-.3)/.35) d.line((78,yy+100,1002,yy+100),fill=(193,198,185),width=2) label(im,title,78,yy+22,29,INK) xx=932 d.ellipse((xx-31,yy+10,xx+31,yy+72),outline=INK,width=2) if p>.9: d.ellipse((xx-31,yy+10,xx+31,yy+72),fill=BLUE) d.line((xx-14,yy+40,xx-2,yy+52,xx+17,yy+29),fill=PAPER,width=5) else: d.arc((xx-20,yy+21,xx+20,yy+61),int(t*280),int(t*280)+220,fill=BLUE,width=4) label(im,"CONFIDENCE COMES FROM CHECKING.",78,1593,25,INK) elif idx == 7: grid(im,(20,31,32),108,-t*24) im=torus(im,t*1.4,center=(540,1230),scale=230,color=(150,173,255)) headline(im,["AGAIN.","SHARPER.","AGAIN."],350,165,age=age,accent=LIME,gap=35) d=ImageDraw.Draw(im) lap=int(age/BEAT)+1 label(im,f"ITERATION {lap:02d}",540,1203,31,LIME,align="center") label(im,"SMALL CHANGES. BIG DIFFERENCE.",78,1640,25,PAPER) for k in range(8): x=113+k*112 d.rectangle((x,1570,x+70,1578),fill=LIME if k=delay,np.exp(-np.maximum(t-delay,0)*110),0) return .24*bright*env+.08*noise*clap+.18*np.sin(math.tau*185*t)*np.exp(-t*32) def hat(opened=False): t=ts(.17 if opened else .058) noise=rng.normal(size=len(t)) high=np.diff(noise,prepend=0) return high*np.exp(-t*(24 if opened else 90))*.12 def bass(midi,dur): t=ts(dur) f=freq(midi) wave=np.sin(math.tau*f*t) for harmonic in [2,3,4,5,7]: wave+=.35/harmonic*np.sin(math.tau*f*harmonic*t)*np.exp(-t*harmonic*4) env=(1-np.exp(-t*350))*np.minimum(1,(dur-t)/.035)*np.exp(-t*2) return np.tanh(wave*2)*env*.48 def pluck(midi,dur=.36): t=ts(dur) f=freq(midi) env=(1-np.exp(-t*700))*np.exp(-t*11)*np.minimum(1,(dur-t)/.025) sig=np.sin(math.tau*f*t)+.3*np.sin(math.tau*f*2.002*t)+.17*np.sin(math.tau*f*3*t) return sig*env*.21 # Sparse, anticipatory opening. for b in [0,1,2,3,3.5]: add("fx",pluck(74+int(b)%3*3,.12),b*BEAT,.5,pan=(-.6 if b%2 else .6),echoes=True) roots=[38,34,41,36] # D, Bb, F, C. for b in range(4,68): breakdown=44<=b<52 if not breakdown or b%4==0: add("drums",kick(),b*BEAT,.88 if not breakdown else .62) if b%2==1 and not breakdown: add("drums",snare(),b*BEAT,.76) if not breakdown: add("drums",hat(),b*BEAT,.48,-.22) add("drums",hat(True), (b+.5)*BEAT,.57,.28) if b>=52 or b%4==3: add("drums",hat(),(b+.75)*BEAT,.33,-.4) root=roots[((b-4)//8)%4] if not breakdown: for pos, note, gain in [(.03,root,.9),(.5,root,.8),(.75,root+12,.38)]: add("bass",bass(note,BEAT*.22 if pos==.75 else BEAT*.43),(b+pos)*BEAT,gain) # Syncopated arpeggio with dotted-eighth ping-pong delay. arps=[ [0,7,12,15,12,7,10,7], [0,7,12,16,12,7,11,7], [0,7,12,16,12,7,9,7], [0,7,12,16,12,7,10,7], ] for k in range(8,136): b=k/2 chord=max(0,int((b-4)//8))%4 root=roots[chord]+24 level=.85 if 20<=b<44 or b>=52 else .5 if 44<=b<48 and k%4: continue add("music",pluck(root+arps[chord][k%8]),b*BEAT,level,pan=.5*math.sin(k*1.7),echoes=True) # Soft, broad synth chords behind the percussive lead. for b in range(4,68,8): root=roots[((b-4)//8)%4]+24 third=3 if root%12==2 else 4 dur=BEAT*7.8 t=ts(dur) env=np.minimum(t/.2,1)*np.minimum((dur-t)/.45,1) for note in [root,root+third,root+7]: f=freq(note) sig=(np.sin(math.tau*f*.998*t)+np.sin(math.tau*f*1.002*t))*.052*env add("music",sig,b*BEAT,.7,pan=(note-root-3)/12) # Risers, impacts, and a short snare build lock to the editorial cuts. for end in [4,12,20,28,36,52,60,68]: dur=BEAT*(2 if end in [4,20,52,68] else 1) t=ts(dur) p=t/dur noise=rng.normal(size=len(t)) noise=(noise+np.roll(noise,1)+np.roll(noise,2))/3 sig=(noise*.16+np.sin(math.tau*(220*t+600*t*t/dur))*.026)*p**2*np.minimum((dur-t)/.03,1) add("fx",sig,end*BEAT-dur,1,pan=-.2) t=ts(.62) impact=(rng.normal(size=len(t))*.14+np.sin(math.tau*(44*t+24*.04*(1-np.exp(-t/.04))))*.25)*np.exp(-t*7) add("fx",impact,end*BEAT,.7) for k in range(12): add("drums",snare(),(49+k*.25)*BEAT,.18+k*.025,pan=(-.2 if k%2 else .2)) # Final chord and sub impact ring out under the closing card. add("drums",kick(),68*BEAT,1.) for note in [50,62,65,69,74]: t=ts(1.6667) sig=np.sin(math.tau*freq(note)*t)*np.exp(-t*2.3)*(1-np.exp(-t*200))*.14 add("music",sig,68*BEAT,1,pan=(note-62)/24) timeline=np.arange(n)/sr duck=1-.55*np.exp(-((timeline/BEAT)%1)*8) buses["music"]*=duck[:,None] buses["bass"]*=duck[:,None] master=buses["drums"]+buses["bass"]+buses["music"]+buses["fx"] master=np.tanh(master*.92) fadein=np.minimum(timeline/.016,1) fadeout=np.minimum((DURATION-timeline)/.48,1) master*=fadein[:,None]*fadeout[:,None] master*=.91/np.max(np.abs(master)) pcm=(master*32767).astype("