"""The twenty-four swarms without films, each woken for a fraction of a second on archive plates and the Small Sun cast. Each demo shows its swarm's actual mechanism, not a label: MLS warps, weights, bones, masks, layers, refusal.""" import numpy as np, math from PIL import Image, ImageDraw, ImageFont from scipy.ndimage import map_coordinates, gaussian_filter ROOT = '/home/claude/afterlight/trailer3' w, h = 640, 360 PHOS = (120, 255, 160); AMB = (255, 176, 70); CRIM = (230, 60, 50); GOLD = (255, 214, 130) _cache = {} def plate(name): if name not in _cache: _cache[name] = np.asarray(Image.open(f'{ROOT}/plates/{name}.png').convert('RGB').resize((w, h), Image.BILINEAR), np.float32) / 255 return _cache[name] def sprite(name, scale=0.5): k = (name, scale) if k not in _cache: im = Image.open(f'{ROOT}/sprites/{name}.png').convert('RGBA'); im = im.resize((int(im.width * scale), int(im.height * scale)), Image.BILINEAR) _cache[k] = np.asarray(im, np.float32) / 255 return _cache[k] def blank(c=(0.02, 0.02, 0.03)): return np.zeros((h, w, 3), np.float32) + np.array(c, np.float32) def comp(dst, rgba, x, y): """alpha-composite rgba (hh,ww,4) with top-left at x,y""" hh, ww = rgba.shape[:2]; x0, y0 = int(x), int(y); x1, y1 = max(x0, 0), max(y0, 0); x2, y2 = min(x0 + ww, w), min(y0 + hh, h) if x2 <= x1 or y2 <= y1: return dst s = rgba[y1 - y0:y2 - y0, x1 - x0:x2 - x0]; a = s[..., 3:4] dst[y1:y2, x1:x2] = dst[y1:y2, x1:x2] * (1 - a) + s[..., :3] * a; return dst def rotate_rgba(rgba, deg, center=None): im = Image.fromarray((rgba * 255).astype(np.uint8), 'RGBA') return np.asarray(im.rotate(deg, resample=Image.BILINEAR, center=center), np.float32) / 255 def warp(img, bx, by): # backward map: output pixel samples img at (bx, by) return np.stack([map_coordinates(img[..., c], [by, bx], order=1, mode='constant', cval=0.02) for c in range(img.shape[2])], -1) YY, XX = np.mgrid[0:h, 0:w].astype(np.float32) def mls(p, q, v_x, v_y, kind='affine', alpha=1.0): """Schaefer et al. 2006 moving least squares. maps points v near handles p to follow q. returns mapped x, y""" p = np.asarray(p, np.float32); q = np.asarray(q, np.float32) V = np.stack([v_x.ravel(), v_y.ravel()], 1) d2 = ((V[:, None, :] - p[None]) ** 2).sum(-1) + 1e-3; wgt = 1 / d2 ** alpha sw = wgt.sum(1, keepdims=True); ps = (wgt @ p) / sw; qs = (wgt @ q) / sw ph = p[None] - ps[:, None]; qh = q[None] - qs[:, None] if kind == 'affine': A = np.einsum('ni,nij,nik->njk', wgt, ph, ph) B = np.einsum('ni,nij,nik->njk', wgt, ph, qh) M = np.linalg.solve(A + np.eye(2) * 1e-3, B) out = np.einsum('nj,njk->nk', V - ps, M) + qs else: # similarity / rigid vp = V - ps ph_perp = np.stack([-ph[..., 1], ph[..., 0]], -1) mu = (wgt * (ph ** 2).sum(-1)).sum(1)[:, None] a = (wgt * (ph * qh).sum(-1)).sum(1); b = (wgt * (ph_perp * qh).sum(-1)).sum(1) if kind == 'rigid': nrm = np.sqrt(a * a + b * b)[:, None] + 1e-6 else: nrm = mu c, s = a[:, None] / nrm, b[:, None] / nrm out = np.stack([vp[:, 0] * c[:, 0] - vp[:, 1] * s[:, 0], vp[:, 0] * s[:, 0] + vp[:, 1] * c[:, 0]], 1) + qs return out[:, 0].reshape(v_x.shape), out[:, 1].reshape(v_y.shape) def thermal(t): t = np.clip(t, 0, 1)[..., None] stops = np.array([[0.03, 0.01, 0.08], [0.32, 0.03, 0.22], [0.70, 0.07, 0.07], [0.97, 0.42, 0.06], [1.0, 0.86, 0.52]], np.float32) x = t * 4; i = np.clip(np.floor(x).astype(int), 0, 3); f = x - i return stops[i[..., 0]] * (1 - f) + stops[i[..., 0] + 1] * f def lum(img): return img @ np.array([0.3, 0.59, 0.11], np.float32) def grade(img, heat=0.35): return img * (1 - heat) + thermal(lum(img) * 0.95) * heat def overlay(img): pil = Image.fromarray((np.clip(img, 0, 1) * 255).astype(np.uint8)); return pil, ImageDraw.Draw(pil, 'RGBA') def back(pil): return np.asarray(pil, np.float32) / 255 def grid_lines(d, fx, fy, step=24, col=PHOS, a=110): for gy in range(0, h + 1, step): xs = np.arange(0, w + 1, 8, dtype=np.float32); pts = list(zip(*fx(xs, np.full_like(xs, gy)))); d.line(pts, fill=col + (a,), width=1) for gx in range(0, w + 1, step): ys = np.arange(0, h + 1, 8, dtype=np.float32); pts = list(zip(*fx(np.full_like(ys, gx), ys))); d.line(pts, fill=col + (a,), width=1) def ease(u): u = min(1, max(0, u)); return u * u * (3 - 2 * u) # ---------------- the demos (u: 0..1 through the cut; t: film time; e: audio energy 0..1) ---------------- def d10(u, t, e): # MLS deformation (affine) on the mirror plate img = plate('mirror'); p = np.array([[240, 120], [400, 110], [250, 280], [420, 270], [330, 190]], np.float32) off = np.array([[-30, -20], [40, -30], [-20, 30], [50, 20], [0, 0]], np.float32) * math.sin(u * math.pi * 1.2) q = p + off; bx, by = mls(q, p, XX, YY, 'affine') out = grade(warp(img, bx, by), 0.3); pil, d = overlay(out) fx = lambda x, y: mls(p, q, x, y, 'affine'); grid_lines(d, fx, None, 32, PHOS, 70) for a_, b_ in zip(p, q): d.line([tuple(a_), tuple(b_)], fill=AMB + (220,), width=2); d.ellipse([b_[0] - 5, b_[1] - 5, b_[0] + 5, b_[1] + 5], outline=AMB + (255,), width=2) return back(pil) def d35(u, t, e): # MLS gesture warp: one handle draws a gesture through the poppy img = plate('poppy'); p = np.array([[320, 150], [120, 60], [520, 60], [120, 320], [520, 320]], np.float32) ang = u * 5.5; g = np.array([320 + 70 * math.cos(ang), 150 + 50 * math.sin(ang * 1.3)], np.float32) q = p.copy(); q[0] = g; bx, by = mls(q, p, XX, YY, 'similarity') out = grade(warp(img, bx, by), 0.25); pil, d = overlay(out) trail = [(320 + 70 * math.cos(a), 150 + 50 * math.sin(a * 1.3)) for a in np.linspace(0, ang, 60)] d.line(trail, fill=PHOS + (200,), width=2); d.ellipse([g[0] - 6, g[1] - 6, g[0] + 6, g[1] + 6], outline=PHOS + (255,), width=2) return back(pil) def d34(u, t, e): # as-rigid-as-possible: the runner bends, the triangles keep their shape img = plate('runner'); p = np.array([[330, 150], [330, 330], [200, 240], [460, 240]], np.float32) s = math.sin(u * math.pi); q = p + np.array([[60 * s, -20 * s], [0, 0], [-10, 0], [10, 0]], np.float32) bx, by = mls(q, p, XX, YY, 'rigid'); out = grade(warp(img, bx, by), 0.3); pil, d = overlay(out) vx, vy = np.meshgrid(np.arange(230, 431, 25, dtype=np.float32), np.arange(120, 341, 25, dtype=np.float32)) mx, my = mls(p, q, vx, vy, 'rigid') for i in range(vx.shape[0] - 1): for j in range(vx.shape[1] - 1): a = (mx[i, j], my[i, j]); b = (mx[i, j + 1], my[i, j + 1]); c = (mx[i + 1, j], my[i + 1, j]); dd = (mx[i + 1, j + 1], my[i + 1, j + 1]) d.polygon([a, b, c], outline=PHOS + (120,)); d.polygon([b, dd, c], outline=PHOS + (120,)) for b_ in q: d.ellipse([b_[0] - 5, b_[1] - 5, b_[0] + 5, b_[1] + 5], fill=AMB + (255,)) return back(pil) def d12(u, t, e): # localized deformation weights: the flower head bends; the influence stays inside the flower img = plate('poppy'); m = gaussian_filter((lum(img) > 0.18).astype(np.float32), 2) hx, hy = 330, 120; dist = np.sqrt((XX - hx) ** 2 + (YY - hy) ** 2); wgt = np.exp(-dist / 90) * m s = math.sin(u * math.pi * 2) * 40; bx = XX - s * wgt; by = YY + abs(s) * 0.3 * wgt out = warp(img, bx, by); wv = warp(wgt[..., None], bx, by)[..., 0] out = out * (1 - 0.6 * (wv[..., None] > 0.03)) + thermal(wv * 1.1) * 0.6 * (wv[..., None] > 0.03) pil, d = overlay(out); d.ellipse([hx + s - 8, hy - 8, hx + s + 8, hy + 8], outline=PHOS + (255,), width=3); d.line([(hx, hy), (hx + s, hy)], fill=PHOS + (200,), width=2) return back(pil) def d36(u, t, e): # bounded biharmonic weights: four handles, each owning a smooth, bounded share of the coins img = plate('crystal'); m = gaussian_filter((lum(img) > 0.35).astype(np.float32), 1.5) H_ = np.array([[150, 110], [280, 220], [430, 120], [520, 260]], np.float32); cols = np.array([[1, .3, .2], [1, .75, .2], [.4, 1, .6], [.45, .65, 1]], np.float32) D = np.stack([1 / (1e-3 + ((XX - x) ** 2 + (YY - y) ** 2) / 6000) ** 2 for x, y in H_], -1); Wt = D / D.sum(-1, keepdims=True) off = np.array([[math.sin(u * 6 + k * 1.7) * 26, math.cos(u * 5 + k) * 18] for k in range(4)], np.float32) bx = XX - (Wt * off[:, 0]).sum(-1); by = YY - (Wt * off[:, 1]).sum(-1) base = warp(img, bx, by); mm = warp(m[..., None], bx, by); col = (Wt[..., None] * cols).sum(-2) out = base * 0.35 + col * mm * (0.35 + 0.65 * lum(base)[..., None]) + base * 0.3 * (1 - mm); pil, d = overlay(out) for (x, y), o, c in zip(H_, off, cols): d.ellipse([x + o[0] - 8, y + o[1] - 8, x + o[0] + 8, y + o[1] + 8], outline=tuple(int(v * 255) for v in c) + (255,), width=3) return back(pil) def d37(u, t, e): # weighted skinning: two bones bend the boy with the reel; the seam blends img = plate('reel'); a = math.sin(u * math.pi) * 0.5; pivot = (320, 200) wgt = 1 / (1 + np.exp(-(XX - 320) / 28)) # skin weight: 0 on the left bone, 1 on the right ca, sa = math.cos(-a), math.sin(-a); rx = (XX - pivot[0]) * ca - (YY - pivot[1]) * sa + pivot[0]; ry = (XX - pivot[0]) * sa + (YY - pivot[1]) * ca + pivot[1] bx = XX * (1 - wgt) + rx * wgt; by = YY * (1 - wgt) + ry * wgt out = grade(warp(img, bx, by), 0.35); band = np.exp(-((XX - 320) / 30) ** 2)[..., None] out = out * (1 - band * 0.35) + thermal(np.full((h, w), 0.8)) * band * 0.35; pil, d = overlay(out) end = (pivot[0] + 200 * math.cos(a), pivot[1] - 200 * math.sin(a)); d.line([(120, 200), pivot], fill=PHOS + (255,), width=4); d.line([pivot, end], fill=AMB + (255,), width=4) d.ellipse([pivot[0] - 7, pivot[1] - 7, pivot[0] + 7, pivot[1] + 7], fill=GOLD + (255,)); return back(pil) def d32(u, t, e): # forward kinematics: a chain cut from the hand plate; turn the shoulder and the rest follows img = plate('hand'); out = grade(img * 0.25, 0.4); pil = Image.fromarray((out * 255).astype(np.uint8)).convert('RGBA'); d = ImageDraw.Draw(pil, 'RGBA') src = Image.fromarray((img * 255).astype(np.uint8)).convert('RGBA'); x, y, ang = 120.0, 180.0, 0.0 for k in range(4): ang += math.sin(u * math.pi * 2 + k * 0.6) * (0.5 - k * 0.08); seg = src.crop((120 + k * 110, 150, 240 + k * 110, 210)).rotate(-math.degrees(ang), expand=True, resample=Image.BILINEAR) nx, ny = x + 110 * math.cos(ang), y + 110 * math.sin(ang); cx, cy = (x + nx) / 2, (y + ny) / 2 pil.alpha_composite(seg, (int(cx - seg.width / 2), int(cy - seg.height / 2))) d.line([(x, y), (nx, ny)], fill=PHOS + (230,), width=2); d.ellipse([x - 6, y - 6, x + 6, y + 6], outline=AMB + (255,), width=2); x, y = nx, ny return np.asarray(pil.convert('RGB'), np.float32) / 255 def d19(u, t, e): # smart bones: the bend triggers a corrective shape (the elbow bulges instead of collapsing) img = plate('hand'); a = math.sin(u * math.pi) * 0.7; px, py = 330, 190 side = XX > px; ca, sa = math.cos(-a), math.sin(-a) rx = np.where(side, (XX - px) * ca - (YY - py) * sa + px, XX); ry = np.where(side, (XX - px) * sa + (YY - py) * ca + py, YY) bul = np.exp(-(((XX - px) / 40) ** 2 + ((YY - py) / 60) ** 2)) * 18 * abs(math.sin(u * math.pi)); ry = ry + bul out = grade(warp(img, rx, ry), 0.35); pil, d = overlay(out) d.line([(150, 190), (px, py)], fill=PHOS + (255,), width=3); d.line([(px, py), (px + 170 * math.cos(a), py - 170 * math.sin(a))], fill=PHOS + (255,), width=3) d.arc([px - 30, py - 30, px + 30, py + 30], 0, -math.degrees(a) or -1, fill=AMB + (255,), width=2); return back(pil) def piece(spr, y0, y1): p = np.zeros_like(spr); p[y0:y1] = spr[y0:y1]; return p def d20(u, t, e): # production rig: the girl as pegged pieces, exploded and reassembled out = blank((0.03, 0.03, 0.05)); spr = sprite('girl_stand', 0.55); hh, ww = spr.shape[:2]; sx, sy = 230, 15 cuts = [0, int(hh * 0.3), int(hh * 0.62), hh]; ex = math.sin(u * math.pi) * 34; pil = None for k in range(3): pc = piece(spr, cuts[k], cuts[k + 1]); ang = math.sin(u * 7 + k) * 5 * math.sin(u * math.pi) pc = rotate_rgba(pc, ang, (ww / 2, cuts[k])); comp(out, pc, sx, sy + (k - 1) * ex) pil, d = overlay(out) for k in range(3): y = sy + cuts[k] + (k - 1) * ex; d.line([(sx + ww / 2 - 10, y), (sx + ww / 2 + 10, y)], fill=PHOS + (255,), width=2); d.line([(sx + ww / 2, y - 10), (sx + ww / 2, y + 10)], fill=PHOS + (255,), width=2) d.line([(sx + ww / 2 + 14, y), (560, 80 + k * 70)], fill=PHOS + (70,), width=1); d.rectangle([560, 70 + k * 70, 620, 92 + k * 70], outline=AMB + (200,), width=1) return back(pil) def d23(u, t, e): # skeletal runtime: two clips blend on the cat (idle and alert), bones drawn over out = blank(); a = sprite('cat_stand', 0.5); b = sprite('cat_sit', 0.5); mix = ease((math.sin(u * math.pi * 2) + 1) / 2) out = comp(comp(out.copy(), a * np.array([1, 1, 1, 1 - mix], np.float32), 150, 60), b * np.array([1, 1, 1, mix], np.float32), 150, 60) pil, d = overlay(out); J = [(230, 230), (320, 205), (420, 200), (470, 170), (500, 150)] d.line(J, fill=PHOS + (230,), width=2); [d.ellipse([x - 4, y - 4, x + 4, y + 4], fill=AMB + (255,)) for x, y in J] d.rectangle([60, 330, 580, 334], fill=(40, 60, 50, 255)); d.rectangle([60, 330, 60 + 520 * u, 334], fill=PHOS + (255,)); return back(pil) def d21(u, t, e): # photo to actor: the face plate gets landmarks, a mesh, and a nod it never made img = plate('face'); a = math.sin(u * math.pi * 2) * 0.06; c = (300, 330) rx = (XX - c[0]) * math.cos(a) - (YY - c[1]) * math.sin(a) + c[0]; ry = (XX - c[0]) * math.sin(a) + (YY - c[1]) * math.cos(a) + c[1] out = grade(warp(img, rx, ry), 0.3); pil, d = overlay(out) lm = [(210, 96), (400, 80), (300, 150), (240, 210), (330, 205), (160, 140), (450, 125)] rot = lambda p: (c[0] + (p[0] - c[0]) * math.cos(-a) - (p[1] - c[1]) * math.sin(-a), c[1] + (p[0] - c[0]) * math.sin(-a) + (p[1] - c[1]) * math.cos(-a)) L_ = [rot(p) for p in lm] for i, j in [(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (0, 5), (1, 6)]: d.line([L_[i], L_[j]], fill=PHOS + (150,), width=1) for p in L_: d.ellipse([p[0] - 3, p[1] - 3, p[0] + 3, p[1] + 3], fill=AMB + (255,)) return back(pil) def d22(u, t, e): # live performance capture: the face follows the voice; the waveform drives the head img = plate('face'); b = (0.3 + e) * 40 * abs(math.sin(t * 9)); out = grade(warp(img, XX - b * 0.3, YY - b * np.exp(-((YY - 150) / 140) ** 2)), 0.3); pil, d = overlay(out) xs = np.arange(40, 600, 4); ys = 320 + np.sin(xs * 0.11 + t * 30) * 30 * (0.3 + e) * np.sin(xs / 600 * math.pi) d.line(list(zip(xs, ys)), fill=PHOS + (240,), width=4); d.ellipse([20, 308, 40, 328], fill=CRIM + (255,)); return back(pil) def d13(u, t, e): # secondary motion: the girl stops; her ribbons keep going out = blank(); spr = sprite('girl_arms', 0.5); mv = 150 * ease(min(1, u * 1.6)); x = 140 + mv; comp(out, spr, x, 30) pil, d = overlay(out); hx, hy = x + spr.shape[1] * 0.72, 30 + spr.shape[0] * 0.43 for k in range(4): pts = []; px, py = hx, hy for s in range(10): lag = (s + 1) * 0.03; v = 1.6 * 150 * max(0, 1 - abs(u * 1.6 - lag * 3)) if u < 0.7 else 0 wob = math.sin((u - lag) * 18 + k) * (6 + s * 2.5) * math.exp(-max(0, u - 0.6) * 4) px += -v * 0.02 - 2 + wob * 0.3; py += 9; pts.append((px + wob, py)) d.line([(hx, hy)] + pts, fill=[CRIM, AMB, GOLD, PHOS][k] + (230,), width=3) return back(pil) def d15(u, t, e): # learned layered puppet: three layers inferred from one image, pulled apart img = plate('boy'); L_ = lum(img); layers = [L_ < 0.33, (L_ >= 0.33) & (L_ < 0.62), L_ >= 0.62] out = blank(); s = math.sin(u * math.pi) for k, m in enumerate(layers): dx = (k - 1) * 40 * s; dy = (1 - k) * 12 * s; mm = np.roll(np.roll(m, int(dx), 1), int(dy), 0) tint = np.array([[0.75, 0.12, 0.10], [0.95, 0.55, 0.12], [1.0, 0.9, 0.6]][k], np.float32) src = np.roll(np.roll(img, int(dx), 1), int(dy), 0) out = np.where(mm[..., None], src * 0.5 + tint * 0.5 * (0.4 + 0.6 * s) + src * 0.5 * (1 - s), out) return out def d42(u, t, e): # few-shot structure: four source frames, one shared structure inferred, tested out = blank(); names = ['face', 'boy', 'mirror', 'digging']; pil, d = overlay(out) for k, n in enumerate(names): im = Image.fromarray((grade(plate(n), 0.4) * 255).astype(np.uint8)).resize((120, 68)); pil.paste(im, (20, 20 + k * 82)); d.rectangle([20, 20 + k * 82, 140, 88 + k * 82], outline=AMB + (200,)) d.line([(142, 54 + k * 82), (300, 180)], fill=PHOS + (int(60 + 160 * ease(u * 2 - k * 0.2)),), width=1) J = [(380, 90), (380, 170), (340, 230), (420, 230), (350, 300), (410, 300)]; prog = ease(u * 1.5) for i, j in [(0, 1), (1, 2), (1, 3), (1, 4), (1, 5)]: a, b = J[i], J[j]; d.line([a, (a[0] + (b[0] - a[0]) * prog, a[1] + (b[1] - a[1]) * prog)], fill=PHOS + (255,), width=3) d.ellipse([360, 60, 400, 100], outline=GOLD + (255,), width=2); return back(pil) def d26(u, t, e): # archive stage: hundreds of fragments find their place img = grade(plate('runner'), 0.25); out = blank(); rng = np.random.default_rng(26); s = 20 for k in range(int(w / s * h / s)): gx, gy = (k % (w // s)) * s, (k // (w // s)) * s; r = rng.random(); arr = ease(u * 1.5 - r * 0.5) sx, sy = rng.random() * w, rng.random() * h; x = sx + (gx - sx) * arr; y = sy + (gy - sy) * arr if 0 <= x < w - s and 0 <= y < h - s: out[int(y):int(y) + s, int(x):int(x) + s] = img[gy:gy + s, gx:gx + s] * (0.4 + 0.6 * arr) return out def d28(u, t, e): # mask and plate: a tracked oval mask lifts the mirror out of its plate img = plate('mirror'); cx, cy = 255 + math.sin(u * 4) * 6, 205; rx, ry = 70 + 30 * ease(u * 1.4), 90 + 30 * ease(u * 1.4) m = ((XX - cx) / rx) ** 2 + ((YY - cy) / ry) ** 2; mask = np.clip((1.15 - m) / 0.3, 0, 1)[..., None] out = img * 0.18 + grade(img, 0.15) * mask * 0.82 + thermal(lum(img) * 0.7) * (1 - mask) * 0.25; pil, d = overlay(out) d.ellipse([cx - rx, cy - ry, cx + rx, cy + ry], outline=PHOS + (255,), width=2) for a in np.linspace(0, 2 * math.pi, 9)[:-1]: px, py = cx + rx * math.cos(a), cy + ry * math.sin(a); d.rectangle([px - 3, py - 3, px + 3, py + 3], outline=AMB + (255,)) d.line([(cx - 8, cy), (cx + 8, cy)], fill=AMB + (255,)); d.line([(cx, cy - 8), (cx, cy + 8)], fill=AMB + (255,)); return back(pil) def d30(u, t, e): # prompted masks: click a point, a region is proposed; nothing invisible is invented img = plate('boy'); small = img[::4, ::4].reshape(-1, 3); rng = np.random.default_rng(30) C_ = small[rng.choice(len(small), 6, replace=False)] for _ in range(6): lab = ((small[:, None] - C_[None]) ** 2).sum(-1).argmin(1); C_ = np.array([small[lab == k].mean(0) if (lab == k).any() else C_[k] for k in range(6)]) full = ((img.reshape(-1, 3)[:, None] - C_[None]) ** 2).sum(-1).argmin(1).reshape(h, w) hues = np.array([[1, .3, .3], [1, .7, .2], [.4, 1, .6], [.4, .7, 1], [.9, .5, 1], [1, 1, .5]], np.float32) k_on = int(u * 6) % 6; m = (full == k_on)[..., None] out = img * 0.6 + m * (hues[k_on] * 0.5 + img * 0.0) ; pil, d = overlay(out) ys, xs = np.nonzero(full == k_on) if len(xs): px, py = int(np.median(xs)), int(np.median(ys)); d.ellipse([px - 6, py - 6, px + 6, py + 6], fill=PHOS + (255,)); d.ellipse([px - 11, py - 11, px + 11, py + 11], outline=PHOS + (200,), width=2) return back(pil) def table(): yy = YY / h; base = np.stack([0.30 + 0.05 * np.sin(XX * 0.05 + np.sin(YY * 0.01) * 3), 0.18 + 0.03 * np.sin(XX * 0.05), 0.10 + 0 * XX], -1) return (base * (0.7 + 0.3 * yy[..., None])).astype(np.float32) def cutout_from(name, thr=0.25, scale=0.55): img = plate(name); m = gaussian_filter((lum(img) < thr).astype(np.float32), 1.5) ys, xs = np.nonzero(m > 0.5); y0, y1, x0, x1 = ys.min(), ys.max(), xs.min(), xs.max() paper = np.concatenate([np.full((y1 - y0, x1 - x0, 3), 0.93, np.float32) * (0.9 + 0.1 * img[y0:y1, x0:x1]), m[y0:y1, x0:x1, None]], -1) im = Image.fromarray((paper * 255).astype(np.uint8), 'RGBA'); im = im.resize((int(im.width * scale), int(im.height * scale))) return np.asarray(im, np.float32) / 255 def paper_puppet(name, scale): spr = sprite(name, scale).copy(); g = lum(spr[..., :3])[..., None] spr[..., :3] = spr[..., :3] * 0.75 + 0.25 * g # printed on paper: slightly faded edge = np.clip(gaussian_filter(spr[..., 3], 2.0) * 1.6, 0, 1) # a white cut margin around the figure out = np.zeros_like(spr); out[..., :3] = 0.94; out[..., 3] = edge a = spr[..., 3:4]; out[..., :3] = out[..., :3] * (1 - a) + spr[..., :3] * a; return out def d11(u, t, e): # video puppetry tabletop: a paper figure performs on the table; its path is captured, the hand is not out = table(); cut = paper_puppet('girl_stand', 0.42); path = [(60 + 330 * ease(k / 30 * u), 50 + abs(math.sin(k / 30 * u * 14)) * -14) for k in range(31)] x, y = path[-1]; sh = cut.copy(); sh[..., :3] = 0; sh[..., 3] *= 0.45; comp(out, sh, x + 12, y + 14); comp(out, rotate_rgba(cut, math.sin(u * 14) * 4), x, y) pil, d = overlay(out); fx = [(px + cut.shape[1] / 2, py + cut.shape[0] - 10) for px, py in path] d.line(fx, fill=PHOS + (230,), width=3); [d.ellipse([a - 3, b - 3, a + 3, b + 3], fill=PHOS + (255,)) for a, b in fx[::5]] d.rectangle([20, 20, 120, 40], outline=CRIM + (255,), width=2); d.ellipse([28, 25, 38, 35], fill=CRIM + (255,)) return back(pil) def d40(u, t, e): # paper performance: the grandmother as a paper puppet, shot on twos between fiducials q = math.floor(u * 12) / 12; out = table(); cut = paper_puppet('gran', 0.42); x = 120 + 120 * ease(q); y = 60 + math.sin(q * 11) * 6 sh = cut.copy(); sh[..., :3] = 0; sh[..., 3] *= 0.4; comp(out, sh, x + 10, y + 12); comp(out, rotate_rgba(cut, math.sin(q * 9) * 6), x, y); pil, d = overlay(out) for cx, cy in [(30, 30), (610, 30), (30, 330), (610, 330)]: d.rectangle([cx - 14, cy - 14, cx + 14, cy + 14], outline=(240, 240, 230, 255), width=3); d.rectangle([cx - 5, cy - 5, cx + 5, cy + 5], fill=(240, 240, 230, 255)) return back(pil) def d44(u, t, e): # multiplane parallax: sky, sea and runner on separate planes; the camera slides and they separate img = plate('runner'); out = blank(); dx = (u - 0.5) sky = YY < 150; sea = (YY >= 150) & (YY < 230); fig = gaussian_filter((lum(img) < 0.22).astype(np.float32), 1) > 0.5 for m, sp, k in [(sky, 10, 0), (sea & ~fig, 60, 1), (~sky & ~sea & ~fig, 140, 2), (fig, 220, 3)]: s = int(dx * sp); src = np.roll(img, s, 1); mm = np.roll(m, s, 1)[..., None] out = np.where(mm, grade(src, 0.15 + 0.1 * k), out) pil, d = overlay(out) for k, (y, sp) in enumerate([(150, 10), (230, 60)]): d.line([(0, y), (w, y)], fill=PHOS + (120,), width=1) for k in range(4): d.rectangle([w - 60, 40 + k * 22, w - 60 + 14 + k * 10, 50 + k * 22], fill=AMB + (200,)) return back(pil) def d48(u, t, e): # characters that can refuse: told to walk off the edge, she stops and turns back out = blank((0.02, 0.025, 0.03)); state = 'WALK' if u < 0.45 else ('STOP' if u < 0.62 else 'REFUSE') x = 120 + 260 * ease(min(u, 0.45) / 0.45); face = 1 if u < 0.62 else -1 spr = sprite('girl_stand', 0.5); spr = spr if face > 0 else spr[:, ::-1]; comp(out, spr, x, 40); pil, d = overlay(out) d.line([(560, 330), (560, 20)], fill=CRIM + (200,), width=2) f = ImageFont.truetype('/home/claude/afterlight/trailer/fonts/courier-prime-latin-400-normal.ttf', 18) for k, s in enumerate(['WALK', 'STOP', 'REFUSE']): d.text((20, 20 + k * 24), ('> ' if s == state else ' ') + s, font=f, fill=(PHOS if s == state else (60, 90, 70)) + (255,)) return back(pil) def d49(u, t, e): # open-content reservoir: the plates as a drawer of actors, objects and grounds names = ['barn', 'boy', 'crystal', 'digging', 'eye', 'face', 'fire', 'hand', 'mirror', 'moon', 'neon', 'poppy', 'rays', 'reel', 'road', 'runner', 'silh', 'sunset'] out = blank(); pil, d = overlay(out); off = -u * 260 for k, n in enumerate(names): x = 20 + (k % 6) * 105 + off * (1 if (k // 6) % 2 else -0.6); y = 30 + (k // 6) * 110 im = Image.fromarray((grade(plate(n), 0.3) * 255).astype(np.uint8)).resize((96, 54)); pil.paste(im, (int(x) % 660 - 20, y)); d.rectangle([int(x) % 660 - 20, y, int(x) % 660 + 76, y + 54], outline=AMB + (160,)) return back(pil) def d51(u, t, e): # montage research: cut-outs from everywhere, layering faster than reading out = blank(); rng = np.random.default_rng(int(u * 14)); names = ['face', 'poppy', 'moon', 'hand', 'neon', 'eye', 'sunset', 'crystal', 'boy', 'fire'] for k in range(9): img = plate(names[(k + int(u * 14)) % len(names)]); x0, y0 = int(rng.random() * 400), int(rng.random() * 200); s = int(80 + rng.random() * 160) piece_ = img[y0:y0 + s // 2 * 1, x0:x0 + s]; X0, Y0 = int(rng.random() * (w - s)), int(rng.random() * (h - s // 2)) out[Y0:Y0 + piece_.shape[0], X0:X0 + piece_.shape[1]] = grade(piece_, 0.3) return out DEMOS = {10: d10, 11: d11, 12: d12, 13: d13, 15: d15, 19: d19, 20: d20, 21: d21, 22: d22, 23: d23, 26: d26, 28: d28, 30: d30, 32: d32, 34: d34, 35: d35, 36: d36, 37: d37, 40: d40, 42: d42, 44: d44, 48: d48, 49: d49, 51: d51} def render(num, u, t, e): img = DEMOS[num](u, t, e); im = Image.fromarray((np.clip(img, 0, 1) * 255).astype(np.uint8)).resize((1280, 720), Image.BILINEAR) return np.asarray(im, np.float32) / 255