From 6bae27bab4f03c013800cfd1a362bab8d92c5c90 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 19 Jul 2026 18:32:45 -0700 Subject: [PATCH] sweep: cancelok lineage, reel remix tooling, SineabyeLive tweaks --- cancelok/lineage.json | 151 +++++++++++++++++- marketing/talking-head/bin/reel.mjs | 21 ++- marketing/talking-head/bin/remix-reel.mjs | 150 +++++++++++++++++ .../gui/Sources/SineabyeLive/main.swift | 48 +++++- 4 files changed, 357 insertions(+), 13 deletions(-) create mode 100644 marketing/talking-head/bin/remix-reel.mjs diff --git a/cancelok/lineage.json b/cancelok/lineage.json index f2f2298a71..a0964aeaff 100644 --- a/cancelok/lineage.json +++ b/cancelok/lineage.json @@ -220,8 +220,8 @@ "parent": "ploo", "second": null, "mutation": "invert", - "trait": "gravity-well implosion (collapsing rings + rim-pulled rays + a black singularity) x muffled gong/falling-riser synth", - "born": "2026-07-15T12:08:17.360Z" + "trait": "imploding shockwave / black-hole bloom x reversed cinematic score", + "born": "2026-07-19T20:25:13.389Z" }, "thrum": { "parent": "nuxo", @@ -677,5 +677,152 @@ "mutation": "eye", "trait": "pond-ripple rings x pentatonic harp", "born": "2026-07-18T23:26:49.401Z" + }, + "nembo": { + "parent": null, + "second": null, + "mutation": "blind", + "trait": "elementary cellular automaton cascade (Rule 90 XOR-fractal, rows sonified as they're born) x pentatonic pluck/bell/sub voice with mechanical hat ticks", + "born": "2026-07-19T00:31:20.144Z" + }, + "vosho": { + "parent": "ploo", + "second": null, + "mutation": "voice", + "trait": "concentric shockwave/starburst bloom x flute+harp+bubble breath voice", + "born": "2026-07-19T00:33:53.450Z" + }, + "shivo": { + "parent": "varo", + "second": null, + "mutation": "push", + "trait": "a live moir grid that follows your finger x continuously-detuned pad voice, its beat rate set by drag speed", + "born": "2026-07-19T01:37:03.205Z" + }, + "cogwo": { + "parent": null, + "second": null, + "mutation": "blind", + "trait": "meshed escapement gear-train (ratchet rotation, tooth-lap chimes) x polyrhythmic pluck/bell/sub clockwork", + "born": "2026-07-19T02:49:04.208Z" + }, + "shendo": { + "parent": null, + "second": null, + "mutation": "blind", + "trait": "hyperspace starfield burst (radial perspective streak field, points accelerating outward from a vanishing point) x plucked cosmic arpeggio with a Doppler-panned warp-jump sub", + "born": "2026-07-19T02:53:21.888Z" + }, + "cordo": { + "parent": "nuxo", + "second": "ploo", + "mutation": "cross", + "trait": "string-art harp (nuxo) x riser/drop cinematic voice (ploo)", + "born": "2026-07-19T03:55:56.455Z" + }, + "ondro": { + "parent": "nuxo", + "second": null, + "mutation": "eye", + "trait": "pond ripples (rock-drop rings, spatial rosette by pitch+step) x nuxo's harp/sub/bell/hat voices", + "born": "2026-07-19T03:59:08.738Z" + }, + "yarno": { + "parent": "nuxo", + "second": "ploo", + "mutation": "cross", + "trait": "string-art harp (peg-and-thread bloom) x cinematic riser/drop score", + "born": "2026-07-19T17:02:11.566Z" + }, + "vynko": { + "parent": null, + "second": null, + "mutation": "blind", + "trait": "spinning vinyl record a baked spiral groove read by a swinging tonearm x turntable scratch/pluck voice (drag bends pitch like a real needle)", + "born": "2026-07-19T17:06:52.185Z" + }, + "yavro": { + "parent": null, + "second": null, + "mutation": "blind", + "trait": "sinking-glass-mote caustic-refraction field x whole-tone glass-chime voice", + "born": "2026-07-19T18:10:38.910Z" + }, + "ploxo": { + "parent": "ploo", + "second": "nuxo", + "mutation": "cross", + "trait": "concentric shockwave/starburst bloom x pentatonic harp pluck", + "born": "2026-07-19T18:12:24.661Z" + }, + "volmo": { + "parent": "varo", + "second": null, + "mutation": "eye", + "trait": "dual detuned sine traces + their summed AM envelope (oscilloscope beat diagram) x same varo pad/flute/sub score", + "born": "2026-07-19T19:15:50.230Z" + }, + "harmo": { + "parent": "ploo", + "second": "nuxo", + "mutation": "cross", + "trait": "concentric shockwave/starburst bloom (ploo) x pentatonic harp pluck score (nuxo)", + "born": "2026-07-19T19:18:03.298Z" + }, + "glindo": { + "parent": "wandro", + "second": null, + "mutation": "push", + "trait": "dragged mallet-strum across a pendulum row (wexo) x pluck/bell voice with a Y-axis pitch-bend overtone", + "born": "2026-07-19T20:21:23.888Z" + }, + "glimo": { + "parent": "ploo", + "second": null, + "mutation": "voice", + "trait": "concentric shockwave/starburst bloom x music-box glass chime voice", + "born": "2026-07-19T21:28:01.671Z" + }, + "lissa": { + "parent": "varo", + "second": null, + "mutation": "eye", + "trait": "twin pendulum-wave rows drifting in and out of sync x detuned sine/triangle pad swell", + "born": "2026-07-19T21:32:10.903Z" + }, + "quenlo": { + "parent": null, + "second": null, + "mutation": "blind", + "trait": "Ising-spin annealing lattice (Monte Carlo magnet cooling/reheating into crystallizing domains) x temperature-tied flute drone + magnetization-pitched bell arpeggio", + "born": "2026-07-19T22:36:13.442Z" + }, + "slurvo": { + "parent": "ploo", + "second": null, + "mutation": "push", + "trait": "comet-tail ribbon painter x granular theremin slide", + "born": "2026-07-19T23:44:18.689Z" + }, + "vielo": { + "parent": "ploo", + "second": null, + "mutation": "push", + "trait": "comet-whip harp strings x plucked harp/pluck voices, string-crossing strum", + "born": "2026-07-19T23:47:43.061Z" + }, + "vremo": { + "parent": null, + "second": null, + "mutation": "blind", + "trait": "BSP-cut stained-glass panel that gaps open at its seams on impact x inharmonic stretched-bar chime voice", + "born": "2026-07-20T00:52:54.499Z" + }, + "silvo": { + "parent": "varo", + "second": null, + "mutation": "eye", + "trait": "kaleidoscopic phasor rose vector-sum tip of two detuned orbiting arms, traced as a fading comet trail x same twin-detuned pad swell + sub-bass slide voice as varo", + "born": "2026-07-20T00:57:40.216Z" } } diff --git a/marketing/talking-head/bin/reel.mjs b/marketing/talking-head/bin/reel.mjs index 5462a53436..07c035c659 100644 --- a/marketing/talking-head/bin/reel.mjs +++ b/marketing/talking-head/bin/reel.mjs @@ -171,6 +171,10 @@ const BED_GAIN = parseFloat(flag("bedgain", "0.42")); const ACCENT = flag("accent", "#3dff88"); const ZOOM = parseFloat(flag("zoom", "1.0")); const STAMP_TITLE = flag("title", "prompt.AC"); +const STAMP_SIZE = parseFloat(flag("stamp-size", "145")); +const STAMP_TITLE_PX = parseFloat(flag("stamp-title-px", "72")); +const STAMP_TITLE_SCALE = parseFloat(flag("stamp-title-scale", "0.62")); +const STAMP_CHROMA = parseFloat(flag("stamp-chroma", "3")); const SYNC_ARG = flag("sync", "auto"); const LIMIT = flag("seconds") ? parseFloat(flag("seconds")) : null; const USE_BED = !has("no-bed"); @@ -473,6 +477,10 @@ const stamps = await makeSideStamps({ w: W, h: H, fps: FPS, frames: FRAMES, assetsDir: join(WORK, "stamps"), title: STAMP_TITLE, + stampSize: STAMP_SIZE, + titlePx: STAMP_TITLE_PX, + charScale: STAMP_TITLE_SCALE, + chromaPx: STAMP_CHROMA, }); // The clock's own glyph atlas, drawn top-left each frame (see the loop). @@ -595,10 +603,15 @@ const sampleTint = () => { hueR += (wr / wsum - hueR) * k; hueG += (wg / wsum - hueG) * k; hueB += (wb / wsum - hueB) * k; - return { - core: dayglow([hueR, hueG, hueB]), - hi: dayglow([hueR, hueG, hueB], { value: 255 }).map((c) => Math.min(255, c + 90)), - }; + const seen = dayglow([hueR, hueG, hueB]); + const palette = [[255, 30, 180], [30, 255, 215], [255, 225, 25], [115, 70, 255]]; + const pos = (frame / FPS / 4.5) % palette.length; + const a = palette[Math.floor(pos)]; + const b = palette[(Math.floor(pos) + 1) % palette.length]; + const f = pos - Math.floor(pos); + const cycle = a.map((c, i) => c * (1 - f) + b[i] * f); + const core = seen.map((c, i) => Math.round(c * 0.58 + cycle[i] * 0.42)); + return { core, hi: cycle.map((c) => Math.min(255, Math.round(c + 55))) }; }; const write = (buf) => diff --git a/marketing/talking-head/bin/remix-reel.mjs b/marketing/talking-head/bin/remix-reel.mjs new file mode 100644 index 0000000000..eb40d8c276 --- /dev/null +++ b/marketing/talking-head/bin/remix-reel.mjs @@ -0,0 +1,150 @@ +// remix-reel.mjs — chopped-and-screwed audiovisual finish for a talking reel. +// +// Keeps the intelligible synced reel as its spine, then adds beat-locked vocal +// gates, slowed tape ghosts, heavier low end, RGB separation, temporal trails, +// noise and macroblock tears at the same authored events. +// +// node remix-reel.mjs clean.mp4 --out remix.mp4 + +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +const argv = process.argv.slice(2); +const flag = (n, d = null) => { + const i = argv.indexOf(`--${n}`); + return i >= 0 && argv[i + 1] ? argv[i + 1] : d; +}; +const SRC = resolve(argv.find((a) => !a.startsWith("--")) || ""); +const OUT = resolve(flag("out", "talking-head-remix.mp4")); +if (!existsSync(SRC)) { + console.error("usage: remix-reel.mjs [--out remix.mp4]"); + process.exit(2); +} + +// Phrase-end cuts. Short events chatter; the three longer events throw a +// syrupy slowed ghost forward under the clean vocal. +const chops = [5.10, 11.52, 19.72, 29.12, 33.40, 39.40, 46.00, 53.04, 59.16]; +const screws = [19.72, 39.40, 53.04]; +const between = (t, d) => `between(t,${t.toFixed(2)},${(t + d).toFixed(2)})`; +const any = (times, d) => times.map((t) => between(t, d)).join("+"); + +// Four fast transformer cuts per event. The dry never vanishes completely, so +// consonants remain legible and the remix reads as performance, not corruption. +const gate = chops.map((t) => { + const q = `mod(t-${t.toFixed(2)},0.115)`; + const smooth = `if(lt(${q},0.060),1,if(lt(${q},0.070),` + + `0.59+0.41*cos(PI*(${q}-0.060)/0.010),if(lt(${q},0.109),0.18,` + + `0.59-0.41*cos(PI*(${q}-0.109)/0.006))))`; + return `if(${between(t, 0.46)},${smooth},`; +}).join("") + "1" + ")".repeat(chops.length); + +const screwChains = screws.map((t, i) => + `[sg${i}]atrim=start=${Math.max(0, t - 0.18)}:duration=0.72,asetpts=PTS-STARTPTS,` + + `asetrate=44100*${i === 1 ? "0.70" : "0.76"},aresample=44100,` + + `lowpass=f=4200,volume=${i === 1 ? "0.54" : "0.44"},` + + `adelay=${Math.round(t * 1000)}|${Math.round(t * 1000)}[ghost${i}]`, +).join(";"); +const ghostInputs = screws.map((_, i) => `[ghost${i}]`).join(""); +const adlibs = [ + { at: 18.95, pitch: 0.5, delay: 19850, name: "adLow", volume: 0.28 }, + { at: 38.55, pitch: 2.0, delay: 39600, name: "adHigh", volume: 0.22 }, + { at: 52.15, pitch: 1.4983, delay: 53400, name: "adFifth", volume: 0.25 }, +]; +const adlibChains = adlibs.map((a, i) => { + const stretch = a.pitch >= 1.9 ? ",atempo=0.5,atempo=0.5" : a.pitch > 1 ? ",atempo=0.5" : ""; + return `[ad${i}]atrim=start=${a.at}:duration=0.88,asetpts=PTS-STARTPTS,` + + `asetrate=44100*${a.pitch},aresample=44100${stretch},` + + `aecho=0.72:0.58:135|270|405:0.32|0.19|0.10,highpass=f=110,lowpass=f=8200,` + + `volume=${a.volume},adelay=${a.delay}|${a.delay}[${a.name}]`; +}).join(";"); + +// Two sub-heavy ceremonial impacts and three inharmonic gong blooms announce +// the tape's provenance. They are synthesized locally, not sampled. +const boom = (i, t) => + `aevalsrc='0.92*sin(2*PI*(58*t-18*t*t))*exp(-5.2*t)':s=44100:d=1.1,` + + `lowpass=f=180,adelay=${Math.round(t * 1000)}[boom${i}]`; +const gong = (i, t) => + `aevalsrc='0.13*(sin(2*PI*109*t)+0.7*sin(2*PI*173*t)+0.5*sin(2*PI*281*t))*exp(-0.72*t)':s=44100:d=3.4,` + + `aecho=0.7:0.62:90|173:0.32|0.22,adelay=${Math.round(t * 1000)}[gong${i}]`; +const ceremonies = [boom(0, 0.22), boom(1, 1.48), gong(0, 0.22), gong(1, 1.48), gong(2, 5.10)].join(";"); +const snare = + `anoisesrc=d=62.2:c=white:r=44100,highpass=f=1350,lowpass=f=9200,` + + `volume='if(lt(mod(t-0.83,1.6667),0.18),0.24*exp(-24*mod(t-0.83,1.6667)),0)':eval=frame[snareN];` + + `aevalsrc='if(lt(mod(t-0.83,1.6667),0.20),0.25*sin(2*PI*185*t)*exp(-20*mod(t-0.83,1.6667)),0)':s=44100:d=62.2[snareB]`; + +// Three escalating chorus zones. Doubles retain duration while shifting pitch: +// a bright third-ish voice, a low body double, and two short wide slap copies. +const chorus = `between(t,9.6,19.8)+between(t,27.8,40.2)+between(t,44.8,62.2)`; +// Four warm pop chords, changing every two beats: Cmaj7, Am7, Fmaj7, G(add9). +const chordFreqs = [[130.81,164.81,196.00,246.94],[110.00,130.81,164.81,196.00],[87.31,110.00,130.81,164.81],[98.00,123.47,146.83,220.00]]; +const chordExpr = chordFreqs.map((fs, i) => { + const tone = fs.map((f, j) => `${j ? "0.72*" : ""}sin(2*PI*${f}*t)`).join("+"); + return `between(mod(t,6.6668),${(i*1.6667).toFixed(4)},${((i+1)*1.6667).toFixed(4)})*(${tone})`; +}).join("+"); +const chords = `aevalsrc='0.040*(${chordExpr})':s=44100:d=62.2,lowpass=f=1700,` + + `aecho=0.8:0.7:95|190:0.18|0.10,volume='0.55+0.45*(${chorus})':eval=frame[pad]`; + +const audio = + `[0:a]asplit=${screws.length + adlibs.length + 7}[dry][low][hi][lo][dblL][dblR][air]${screws.map((_, i) => `[sg${i}]`).join("")}${adlibs.map((_, i) => `[ad${i}]`).join("")};` + + `[dry]volume='${gate}':eval=frame,acompressor=threshold=-18dB:ratio=2.4:attack=5:release=140[voice];` + + `[low]lowpass=f=145,bass=g=10:f=82,volume=0.72[sub];` + + `[hi]asetrate=44100*1.1892,aresample=44100,atempo=0.8409,highpass=f=170,` + + `volume='0.29*(${chorus})':eval=frame,pan=stereo|c0=0.25*c0|c1=0.95*c1[harmonyHi];` + + `[lo]asetrate=44100*0.8409,aresample=44100,atempo=1.1892,lowpass=f=7200,` + + `volume='0.23*(${chorus})':eval=frame,pan=stereo|c0=0.95*c0|c1=0.25*c1[harmonyLo];` + + `[dblL]adelay=23|23,volume='0.32*(${chorus})':eval=frame,pan=stereo|c0=c0|c1=0.08*c1[doubleL];` + + `[dblR]adelay=47|47,volume='0.27*(${chorus})':eval=frame,pan=stereo|c0=0.08*c0|c1=c1[doubleR];` + + `[air]highpass=f=4200,aecho=0.7:0.55:74|149:0.28|0.16,volume='0.16*(${chorus})':eval=frame[chorusAir];` + + `${screwChains};${adlibChains};${ceremonies};${snare};${chords};` + + `[voice][sub][harmonyHi][harmonyLo][doubleL][doubleR][chorusAir]${ghostInputs}` + + `${adlibs.map(a => `[${a.name}]`).join("")}[pad][boom0][boom1][gong0][gong1][gong2][snareN][snareB]` + + `amix=inputs=${screws.length + adlibs.length + 15}:duration=first:normalize=0,` + + `adeclick=w=55:o=75:a=2:t=2:b=2:m=s,highpass=f=28,` + + `firequalizer=gain_entry='entry(28,-8);entry(55,1.5);entry(110,0.8);entry(280,-1.2);entry(3200,1.1);entry(9000,1.5);entry(16000,-1)',` + + `stereotools=mlev=1:slev=1.18,acompressor=threshold=-12dB:ratio=1.8:attack=12:release=170,` + + `aexciter=amount=1.2:drive=3.5:ceil=14000,alimiter=limit=0.89:attack=5:release=70,` + + `loudnorm=I=-12.5:TP=-1:LRA=6[aout]`; + +// Visual events share the same clock as the audio cuts. RGB separation and +// temporal trails hit every chop; macroblocks/noise are reserved for the three +// screw-downs, giving the reel dynamics instead of a permanent glitch preset. +const chopOn = any(chops, 0.50); +const screwOn = any(screws, 0.92); +const finish = [ + "eq=contrast=1.10:saturation=1.22", + `rgbashift=rh=11:rv=-2:bh=-13:bv=3:edge=smear:enable='${chopOn}'`, + `tmix=frames=4:weights='1 0.72 0.42 0.20':enable='${chopOn}'`, + `pixelize=w=18:h=10:mode=avg:enable='${screwOn}'`, + `chromashift=cbh=18:crh=-18:edge=smear:enable='${screwOn}'`, + `noise=alls=14:allf=t+u:enable='${screwOn}'`, + "unsharp=5:5:0.55:5:5:0", +].join(","); + +// A 3×3 Brady-Bunch wall lands with the chorus. Each tile gets its own color, +// mirror, crop position, and tiny motion offset so the wall stays alive. +const gridSplits = Array.from({ length: 9 }, (_, i) => `[g${i}]`).join(""); +const tiles = Array.from({ length: 9 }, (_, i) => { + const hue = (i - 4) * 13; + const flip = i % 2 ? ",hflip" : ""; + const zoom = 1.02 + (i % 3) * 0.025; + return `[g${i}]scale=360:640:force_original_aspect_ratio=increase,crop=360:640,` + + `eq=saturation=${(1.05 + (i % 3) * .16).toFixed(2)}:contrast=1.08,` + + `hue=h=${hue}${flip},scale=iw*${zoom}:ih*${zoom},crop=360:640[t${i}]`; +}).join(";"); +const gridInputs = Array.from({ length: 9 }, (_, i) => `[t${i}]`).join(""); +const gridOn = `between(t,9.6,19.8)+between(t,27.8,40.2)+between(t,44.8,62.2)`; +const visuals = `[0:v]split=10[clean]${gridSplits};${tiles};` + + `${gridInputs}xstack=inputs=9:layout=0_0|360_0|720_0|0_640|360_640|720_640|0_1280|360_1280|720_1280[wall];` + + `[clean][wall]overlay=enable='${gridOn}'[picture];[picture]${finish}[vout]`; + +console.log(`· ${chops.length} chop events · ${screws.length} screw ghosts`); +const r = spawnSync("ffmpeg", [ + "-hide_banner", "-loglevel", "error", "-y", "-i", SRC, + "-filter_complex", `${audio};${visuals}`, + "-map", "[vout]", "-map", "[aout]", + "-c:v", "libx264", "-preset", "slow", "-crf", "16", "-pix_fmt", "yuv420p", + "-c:a", "aac", "-b:a", "256k", "-movflags", "+faststart", "-shortest", OUT, +], { stdio: "inherit" }); +if (r.status !== 0) process.exit(r.status || 1); +console.log(`✓ ${OUT}`); diff --git a/pop/nullabye/gui/Sources/SineabyeLive/main.swift b/pop/nullabye/gui/Sources/SineabyeLive/main.swift index 6077c0e938..76c5b47197 100644 --- a/pop/nullabye/gui/Sources/SineabyeLive/main.swift +++ b/pop/nullabye/gui/Sources/SineabyeLive/main.swift @@ -56,20 +56,54 @@ struct WorldView: View { var body: some View { GeometryReader { geo in Canvas { c, size in - let scale = min(size.width, size.height) / 22, center = CGPoint(x:size.width/2, y:size.height/2) - func point(_ x: Double, _ y: Double, _ z: Double) -> CGPoint { CGPoint(x:center.x + (x-y)*scale*0.72, y:center.y + (x+y)*scale*0.34-z*scale*0.72) } + let scale = min(size.width, size.height) / 27, center = CGPoint(x:size.width/2, y:size.height/2) + // Deliberately top-down: this is a framebuffer painting of the audible + // plane, not a perspective picture pretending to be acoustic data. + func point(_ x: Double, _ y: Double, _ z: Double = 0) -> CGPoint { CGPoint(x:center.x + x*scale, y:center.y + y*scale-z*scale*0.08) } + if let f=world.frame { + // Coarse pixel-raycast field. Every cell sums distance loss, measured + // energy, pitch phase, and listening-range falloff from all bodies. + let cell=8.0, half=12.5 + for gy in stride(from:-half,through:half,by:cell/scale) { for gx in stride(from:-half,through:half,by:cell/scale) { + var re=0.0,im=0.0,weight=0.0 + for b in f.bodies { let dx=gx-Double(b.x),dy=gy-Double(b.y),d=sqrt(dx*dx+dy*dy+Double(b.z*b.z)*0.12),e=min(1,hypot(Double(b.left),Double(b.right))*42),hz=max(32,Double(b.pitch)),fall=e/(1+0.17*d*d),phase=Double.pi*2*(world.time*hz-d*hz/343);re+=cos(phase)*fall;im+=sin(phase)*fall;weight+=fall } + let pressure=min(1,sqrt(re*re+im*im)*1.9),density=min(1,weight * 0.9),h=(atan2(im,re)/(.pi*2)+1).truncatingRemainder(dividingBy:1),q=point(gx,gy) + c.fill(Path(CGRect(x:q.x-cell/2,y:q.y-cell/2,width:cell,height:cell)),with:.color(Color(hue:h,saturation:0.38+pressure * 0.52,brightness:0.11+density * 0.72).opacity(0.16+density * 0.56))) + }} + } for q in -10...10 { var p=Path(); p.move(to:point(Double(q),-10,0));p.addLine(to:point(Double(q),10,0));c.stroke(p,with:.color(.cyan.opacity(q == 0 ? 0.28:0.08))); var r=Path();r.move(to:point(-10,Double(q),0));r.addLine(to:point(10,Double(q),0));c.stroke(r,with:.color(.cyan.opacity(q == 0 ? 0.28:0.08))) } guard let f = world.frame else { return } let listener = point(Double(f.lx),Double(f.ly),Double(f.lz)), rr=world.listeningRadius*scale - c.stroke(Path(ellipseIn:CGRect(x:listener.x-rr,y:listener.y-rr*0.48,width:rr*2,height:rr*0.96)),with:.color(.yellow.opacity(0.42)),lineWidth:2) + c.fill(Path(ellipseIn:CGRect(x:listener.x-rr,y:listener.y-rr,width:rr*2,height:rr*2)),with:.color(.yellow.opacity(0.025))) + c.stroke(Path(ellipseIn:CGRect(x:listener.x-rr,y:listener.y-rr,width:rr*2,height:rr*2)),with:.color(.yellow.opacity(0.62)),style:StrokeStyle(lineWidth:2,dash:[5,4])) c.fill(Path(ellipseIn:CGRect(x:listener.x-7,y:listener.y-7,width:14,height:14)),with:.color(.yellow)) - let order = world.voices >= 12 ? Array(f.bodies.indices) : Array([4,0,2,3].prefix(world.voices)) + let selected = world.voices >= 12 ? Array(f.bodies.indices) : Array([4,0,2,3].prefix(world.voices)) + let order = selected.sorted { (f.bodies[$0].x + f.bodies[$0].y - f.bodies[$0].z) < (f.bodies[$1].x + f.bodies[$1].y - f.bodies[$1].z) } for (i,index) in order.enumerated() { let b = f.bodies[index] let energy = hypot(Double(b.left),Double(b.right)), phase=world.time*world.rpmScale*Double.pi*2*76/60 + Double(i)*0.37 let ox=Double(b.x)+sin(phase*(1+Double(i)*0.023))*world.oscillation*0.16, oy=Double(b.y)+cos(phase*(1+Double(i)*0.031))*world.oscillation*0.16 - let p=point(ox,oy,Double(b.z)), low=b.pitch > 0 ? 1-min(1,max(0,log2(Double(b.pitch)/45)/7.5)):0, radius=4+low*10 - var trail=Path();trail.move(to:p);trail.addLine(to:CGPoint(x:p.x-cos(phase)*24*world.oscillation,y:p.y-sin(phase)*12*world.oscillation));c.stroke(trail,with:.color(Color(hue:hue(b.pitch),saturation:0.8,brightness:1).opacity(0.25+min(0.5,energy*25))),lineWidth:2) - c.fill(Path(ellipseIn:CGRect(x:p.x-radius,y:p.y-radius,width:radius*2,height:radius*2)),with:.color(Color(hue:hue(b.pitch),saturation:0.72,brightness:0.95))) + let p=point(ox,oy,Double(b.z)), low=b.pitch > 0 ? 1-min(1,max(0,log2(Double(b.pitch)/45)/7.5)):0, radius=5+low*11 + let color=Color(hue:hue(b.pitch),saturation:0.78,brightness:0.98), dx=listener.x-p.x, dy=listener.y-p.y, screenDistance=max(1,hypot(dx,dy)), ux=dx/screenDistance, uy=dy/screenDistance, nx = -uy, ny = ux + let worldDistance=hypot(hypot(ox-Double(f.lx),oy-Double(f.ly)),Double(b.z-f.lz)), inRange=worldDistance <= world.listeningRadius, audible=min(1,energy*38), visibility=inRange ? 0.18+audible*0.82 : 0.09 + // Expanding radii are the near-future pressure arrivals. A ring + // crossing the yellow receiver radius means that voice is imminent. + let propagation=fmod(world.time*(1.4+min(2.2,Double(max(0,b.pitch))/500))+Double(index)*0.31,max(1,world.listeningRadius*1.65)) + for ring in 0..<3 { let wr=fmod(propagation+Double(ring)*world.listeningRadius * 0.55,max(1,world.listeningRadius*1.65))*scale;c.stroke(Path(ellipseIn:CGRect(x:p.x-wr,y:p.y-wr,width:wr*2,height:wr*2)),with:.color(color.opacity((inRange ? 0.10:0.035)+audible * 0.16)),lineWidth:1+audible*1.5) } + // The center ray is the actual emitter -> listener propagation path. + var ray=Path();ray.move(to:p);ray.addLine(to:listener);c.stroke(ray,with:.color(color.opacity(visibility)),style:StrokeStyle(lineWidth:1+audible*5,lineCap:.round,dash:inRange ? []:[5,7])) + // A ship-facing radiation cone: width is measured stereo imbalance. + let spread=8+18*abs(Double(b.left-b.right))/(Double(b.left+b.right)+0.00001), reach=min(screenDistance,48+audible*100) + var cone=Path();cone.move(to:CGPoint(x:p.x+nx*radius * 0.45,y:p.y+ny*radius * 0.45));cone.addLine(to:CGPoint(x:p.x+ux*reach+nx*spread,y:p.y+uy*reach+ny*spread));cone.addLine(to:CGPoint(x:p.x+ux*reach-nx*spread,y:p.y+uy*reach-ny*spread));cone.closeSubpath();c.fill(cone,with:.color(color.opacity(0.025+audible*0.11))) + // Wave packets move toward the listener; spacing compresses with pitch. + let packetCount=Int(min(9,max(3,screenDistance/22))), speed=0.24+min(0.55,Double(max(0,b.pitch))/1800) + for wave in 0..