mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-14 22:52:11 +02:00
feat(observatory): raw-WebGPU living cognitive field with 5 deterministic demos
The Observatory is a full-bleed, zero-library WebGPU surface that renders the memory graph as a living cognitive field. Five deterministic demo moments driven by a URL contract (?demo=<name>&seed=...&frame=N): recall-path, engram-birth, salience-rescue, forgetting-horizon, firewall. Capture mode (?frame=N) freezes the sim deterministically so the same URL produces identical pixels, the viral-clip primitive. Architecture: bare-metal WebGPU engine (no Three.js), seeded demo clock, per-demo plan + renderer modules, WGSL shaders (simulate, nodes, edges, path, birth particles, rescue, forgetting, firewall) plus a post-processing chain (tone mapping, MIP). DOM is instrument overlays only (telemetry strip, timeline spine, rescue verdict); the layout gives /observatory the same full-bleed bypass as marketing routes so recordings stay clean. Reads the real memory graph. Verified live: svelte-check 939 files 0 errors, 96 observatory unit tests green, all 5 demos load at 108-119fps with zero console errors against the live brain. Known follow-up: engram-birth capture-mode particle cluster needs a render fix pass before it is camera-ready; the other 4 demos are camera-ready. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
898bd336cc
commit
a046c620c8
47 changed files with 9695 additions and 6 deletions
108
docs/launch/backward-trace-animation-storyboard.md
Normal file
108
docs/launch/backward-trace-animation-storyboard.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Backward-Trace Animation — Storyboard ("The X Rocket")
|
||||
|
||||
**Purpose:** the single most shareable launch artifact. A ~15s looping clip,
|
||||
pinned to the top of the launch thread on X and used as the Show HN demo. It
|
||||
shows the ONE thing nothing else does: when a failure hits, Vestige's arrow
|
||||
snaps *backward through time* past the confounders to the quiet change that
|
||||
actually caused it — while a vector search sits stuck on the symptom.
|
||||
|
||||
**Design law (from 2026 viral-demo research):** animate the *mechanism*
|
||||
legibly, no narration needed, payoff visible in the first 1.5s, loops clean, no
|
||||
logo intro. The tldraw / X-algorithm-viz / CodeReel pattern: the demo IS the
|
||||
pitch.
|
||||
|
||||
---
|
||||
|
||||
## The case shown (canonical CauseBench archetype)
|
||||
|
||||
Uses the exact incident SHAPE the benchmark is built on (see
|
||||
`benchmarks/causebench/data/real/*.meta.json`: a config/limit/cert/migration/flag
|
||||
change that shares an *entity* with a later failure but *none of its words*, with
|
||||
a more-recent confounder planted to defeat naive recency).
|
||||
|
||||
Timeline of stored memories (left = 3 weeks ago, right = today):
|
||||
|
||||
| When | Memory | Role |
|
||||
|---|---|---|
|
||||
| 21 days ago | `Lowered connection-pool max from 100 → 20 in db.config.yaml` | **the true cause** (shares entity `db.config.yaml` / the DB, not words) |
|
||||
| 9 days ago | `Renamed UserService to AccountService across the API` | confounder (loud, recent, unrelated) |
|
||||
| 4 days ago | `Bumped Postgres driver to 5.2` | confounder (shares "Postgres", more recent than cause) |
|
||||
| **today** | `PagerDuty: checkout timing out under load — "connection acquisition timeout"` | **the failure (symptom)** |
|
||||
|
||||
Vector search ranks by resemblance → it surfaces the driver bump and the
|
||||
timeout log (they share words like "connection", "Postgres"). It ranks the
|
||||
pool-size change near the *bottom* — that memory shares no vocabulary with
|
||||
"timeout under load". Vestige's Retroactive Salience Backfill reaches backward
|
||||
along the shared entity (the database / `db.config.yaml`) and promotes the
|
||||
pool-size change to #1.
|
||||
|
||||
---
|
||||
|
||||
## Frame-by-frame (15s, 30fps, loops)
|
||||
|
||||
**Beat 0 — 0.0s to 1.5s · THE HOOK (payoff visible immediately)**
|
||||
- A red failure card slams in at the right edge (today): `checkout timing out — connection acquisition timeout`. Subtle shake.
|
||||
- Caption, one line, monospace: `your agent's bug, today →`
|
||||
- (Why first: a scroller must see the payoff-shape in 1.5s or they're gone.)
|
||||
|
||||
**Beat 1 — 1.5s to 4.0s · THE TIMELINE FILLS**
|
||||
- A horizontal time axis draws left→right. Four memory cards fade in at their dates: the pool-size change (far left, dim/dormant), the two confounders (mid), the failure (right, red).
|
||||
- The dormant cause is visibly *faded* — small, low-contrast. It looks unimportant. That's the point.
|
||||
|
||||
**Beat 2 — 4.0s to 6.5s · VECTOR SEARCH TRIES (and fails)**
|
||||
- Label appears: `vector search` with a small magnifying-glass icon.
|
||||
- Three thin gray "similarity" beams shoot from the failure card to the cards that *share words*: the driver bump and (self) the log. A rank badge appears: the true cause gets tagged `#7` in gray, sinking to the bottom.
|
||||
- Caption: `finds what looks like the bug`
|
||||
- Beat ends with a gray ✕ over the search — it never touched the real cause.
|
||||
|
||||
**Beat 3 — 6.5s to 10.5s · THE ARROW SNAPS BACK (the money moment)**
|
||||
- Everything else desaturates. A single bright teal arrow launches from the failure card and travels *right-to-left, backward in time*, deliberately skipping the two confounders (each pings faintly and is passed over — "shares words, not entity" micro-label flickers).
|
||||
- The arrow lands on the dormant pool-size card 21 days back. On contact the card **ignites**: scales up, fills teal, snaps from dim to bright. A link line labeled `same entity: db.config.yaml` connects them.
|
||||
- Caption: `Vestige reaches back to what caused it`
|
||||
- This is the frame that gets clipped and reshared. Make the snap fast and physical (ease-in, slight overshoot).
|
||||
|
||||
**Beat 4 — 10.5s to 13.0s · THE VERDICT**
|
||||
- The promoted cause card shows a teal `#1` badge; the vector `#7` badge greys beside it for contrast.
|
||||
- Two-line stat, big: `root-cause recall@1` / `Vestige 60% · vector search 0%`
|
||||
- Small honest sub-label: `on CauseBench · reproducible`
|
||||
|
||||
**Beat 5 — 13.0s to 15.0s · SIGNATURE + LOOP RESET**
|
||||
- Wordmark `vestige` fades in bottom-left, tiny. Tagline: `memory that finds the cause, not the resemblance.`
|
||||
- Everything gently fades and the failure card is already sliding back in at the right — the loop restart is seamless (no hard cut).
|
||||
|
||||
---
|
||||
|
||||
## Production notes
|
||||
|
||||
- **Format:** build in HTML/CSS/SVG (animated), screen-record to MP4 + GIF.
|
||||
Alternatively Remotion (React) if you want a clean MP4 export pipeline — but
|
||||
the animated-SVG prototype below is enough to record from directly.
|
||||
- **Palette:** teal `#1d9e75` = Vestige/cause/truth; gray `#888780` = vector
|
||||
search/confounders/noise; red `#e24b4a` = the failure only. Two-color
|
||||
discipline + one alarm color. Works on light and dark.
|
||||
- **Text:** monospace for the memory-card content (feels like real logs);
|
||||
sans for captions. No narration — captions carry it, so it works muted in a
|
||||
feed (most X video autoplays silent).
|
||||
- **Honesty guardrails (non-negotiable, same as the chart):** the stat frame
|
||||
must say `60%` with `on CauseBench` attached, and pair it with `vector search
|
||||
0%`. Never imply an industry benchmark. Never show the FALSE cross-session
|
||||
claim.
|
||||
- **Loop length:** 15s is the sweet spot for X autoplay + a clean GIF under
|
||||
~8MB. If the GIF is too heavy, cut Beat 1 to 2s and land at 13s total.
|
||||
|
||||
---
|
||||
|
||||
## The X post it pins to
|
||||
|
||||
> Every AI memory tool is built on vector search. Vector search finds what
|
||||
> *looks like* your bug.
|
||||
>
|
||||
> But a root cause never looks like the bug it creates.
|
||||
>
|
||||
> So I built memory that reaches *backward in time* to the change that actually
|
||||
> caused it. 60% root-cause recall where vector search scores 0%. 🧵👇
|
||||
>
|
||||
> [pinned: the 15s clip]
|
||||
|
||||
(Then the thread: the wall-of-zeros chart, the repro command, the "here's where
|
||||
it's weak" honesty tweet.)
|
||||
282
docs/launch/causal-brain-demo.html
Normal file
282
docs/launch/causal-brain-demo.html
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Vestige — causal brain demo</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
html,body { height:100%; background:#05060a; overflow:hidden; }
|
||||
#stage { position:fixed; inset:0; }
|
||||
canvas { display:block; width:100%; height:100%; }
|
||||
.layer { position:fixed; inset:0; pointer-events:none; }
|
||||
.cap {
|
||||
position:fixed; left:0; right:0; bottom:8.5%;
|
||||
text-align:center; font:500 clamp(15px,2.4vw,26px)/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
|
||||
letter-spacing:.01em; color:#cfe9ff; opacity:0; transition:opacity .5s ease;
|
||||
text-shadow:0 0 24px rgba(30,180,255,.35); padding:0 6%;
|
||||
}
|
||||
.cap.mono { font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace; font-size:clamp(13px,1.9vw,20px); }
|
||||
.verdict {
|
||||
position:fixed; left:50%; top:50%; transform:translate(-50%,-50%); text-align:center; opacity:0;
|
||||
transition:opacity .6s ease; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
|
||||
padding:clamp(18px,3vw,40px) clamp(28px,6vw,80px); border-radius:20px;
|
||||
background:radial-gradient(ellipse at center, rgba(5,7,14,.86) 0%, rgba(5,7,14,.72) 60%, rgba(5,7,14,0) 100%);
|
||||
}
|
||||
.verdict .k { font-size:clamp(13px,1.8vw,18px); color:#9fd0e4; letter-spacing:.16em; text-transform:uppercase; }
|
||||
.verdict .v { font-size:clamp(32px,6.4vw,72px); font-weight:600; margin-top:.12em; line-height:1.05;
|
||||
background:linear-gradient(90deg,#7fe6c0,#6ef0e6,#a6dcff); -webkit-background-clip:text; background-clip:text; color:transparent;
|
||||
filter:drop-shadow(0 0 26px rgba(110,240,220,.45)); }
|
||||
.verdict .s { font-size:clamp(11px,1.5vw,15px); color:#8fb0be; margin-top:.6em; font-family:"SF Mono",ui-monospace,monospace; letter-spacing:.04em; }
|
||||
.wordmark { position:fixed; left:5%; bottom:6%; font:600 clamp(16px,2.2vw,24px)/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
letter-spacing:.28em; color:#5dcaa5; opacity:0; transition:opacity .8s ease; text-shadow:0 0 20px rgba(93,202,165,.4); }
|
||||
.replay { position:fixed; right:16px; bottom:14px; font:400 13px/1 -apple-system,sans-serif; color:#7c8a97;
|
||||
background:rgba(255,255,255,.04); border:.5px solid rgba(255,255,255,.14); border-radius:8px; padding:6px 12px; cursor:pointer; pointer-events:auto; }
|
||||
.replay:hover { background:rgba(255,255,255,.08); color:#cfe9ff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="stage"><canvas id="c"></canvas></div>
|
||||
<div class="layer">
|
||||
<div class="cap mono" id="cap"></div>
|
||||
<div class="verdict" id="verdict">
|
||||
<div class="k">root-cause recall@1</div>
|
||||
<div class="v">Vestige 60% · vector search 0%</div>
|
||||
<div class="s">on CauseBench · reproducible</div>
|
||||
</div>
|
||||
<div class="wordmark" id="wordmark">VESTIGE</div>
|
||||
</div>
|
||||
<button class="replay" id="replay">replay</button>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
|
||||
const cap = document.getElementById('cap'), verdict = document.getElementById('verdict');
|
||||
const wordmark = document.getElementById('wordmark');
|
||||
let W, H, DPR;
|
||||
function viewport(){
|
||||
const w = window.innerWidth || document.documentElement.clientWidth || document.getElementById('stage').clientWidth || 1280;
|
||||
const h = window.innerHeight || document.documentElement.clientHeight || document.getElementById('stage').clientHeight || 720;
|
||||
return { w: Math.max(w, 320), h: Math.max(h, 240) };
|
||||
}
|
||||
function resize(){
|
||||
DPR = Math.min(devicePixelRatio||1, 2);
|
||||
const { w, h } = viewport();
|
||||
W = cv.width = w*DPR; H = cv.height = h*DPR;
|
||||
cv.style.width = w+'px'; cv.style.height = h+'px';
|
||||
}
|
||||
function reflow(){ resize(); if(typeof seedBrain==='function'){ seedBrain(); placeActors(); } }
|
||||
resize(); addEventListener('resize', reflow);
|
||||
if(window.ResizeObserver){ let first=true; new ResizeObserver(()=>{ if(first){first=false;return;} reflow(); }).observe(document.getElementById('stage')); }
|
||||
|
||||
// ---- iridescent thin-film palette: indigo -> teal -> cyan -> magenta rim ----
|
||||
// hand-tuned so the CORE reads indigo/teal (living tissue) and motion shifts it
|
||||
// toward cyan/magenta, never muddy orange. w in [0,1].
|
||||
function spectral(w){
|
||||
w = ((w%1)+1)%1;
|
||||
// 4 anchor colors around the film band, catmull-ish blend
|
||||
const stops = [
|
||||
[0.20,0.28,0.95], // indigo
|
||||
[0.20,0.85,0.90], // cyan-teal
|
||||
[0.45,1.00,0.72], // mint
|
||||
[0.85,0.45,1.00], // magenta rim
|
||||
];
|
||||
const f = w*stops.length, i = Math.floor(f)%stops.length, frac = f-Math.floor(f);
|
||||
const a = stops[i], b = stops[(i+1)%stops.length];
|
||||
return [ a[0]+(b[0]-a[0])*frac, a[1]+(b[1]-a[1])*frac, a[2]+(b[2]-a[2])*frac ];
|
||||
}
|
||||
|
||||
// ---- brain point cloud: two overlapping lobes, denser near centre ----
|
||||
const N = 4200;
|
||||
const pts = [];
|
||||
function seedBrain(){
|
||||
pts.length = 0;
|
||||
const cx = W*0.5, cy = H*0.55, s = Math.min(W,H)*0.30;
|
||||
for(let i=0;i<N;i++){
|
||||
// pick a lobe (clear left/right hemispheres)
|
||||
const lobe = Math.random()<0.5 ? -1 : 1;
|
||||
const r = Math.pow(Math.random(), 0.5); // center-weighted -> dense core
|
||||
const a = Math.random()*Math.PI*2;
|
||||
const ex = 0.62, ey = 0.98;
|
||||
let x = Math.cos(a)*r*ex, y = Math.sin(a)*r*ey;
|
||||
x += lobe*0.40; // hemisphere split
|
||||
// pinch the top (frontal) and round the base so silhouette reads as a brain
|
||||
y *= (1.0 - 0.18*Math.max(0,-y));
|
||||
// sulci wrinkle
|
||||
const wob = 0.06*Math.sin(6*x)*Math.sin(5*y);
|
||||
const px = cx + (x+wob)*s, py = cy + (y+wob)*s;
|
||||
pts.push({
|
||||
hx:px, hy:py, // brain-home
|
||||
x:px + (Math.random()-0.5)*60, y:py + (Math.random()-0.5)*60,
|
||||
vx:0, vy:0,
|
||||
// base hue biased toward the indigo->teal core of the palette
|
||||
base:0.02+Math.random()*0.42,
|
||||
life:Math.random(),
|
||||
r:0.8+Math.random()*1.6,
|
||||
ignite:0, // 0..1 activation glow
|
||||
});
|
||||
}
|
||||
}
|
||||
seedBrain(); addEventListener('resize', seedBrain);
|
||||
|
||||
// ---- named actors (screen positions in brain space) ----
|
||||
function homeAt(fx, fy){ // fractional brain coords -> nearest cloud point
|
||||
const cx=W*0.5, cy=H*0.55, s=Math.min(W,H)*0.30;
|
||||
return { x: cx+fx*s, y: cy+fy*s };
|
||||
}
|
||||
let actors = {};
|
||||
function placeActors(){
|
||||
actors = {
|
||||
cause: homeAt(-0.92, 0.35), // deep left lobe, dormant
|
||||
conf1: homeAt(-0.10,-0.45),
|
||||
conf2: homeAt( 0.30, 0.55),
|
||||
fail: homeAt( 0.92,-0.10), // deep right, the symptom
|
||||
};
|
||||
}
|
||||
placeActors(); addEventListener('resize', placeActors);
|
||||
|
||||
// ---- animation clock / beats ----
|
||||
let t0 = performance.now();
|
||||
const DUR = 15000;
|
||||
let arrow = 0; // 0..1 backward-trace progress
|
||||
let failFlare = 0, vecStall = 0, causeGlow = 0;
|
||||
|
||||
function setCap(s, mono){ cap.textContent = s; cap.className = 'cap'+(mono?' mono':''); cap.style.opacity = s? 1:0; }
|
||||
|
||||
function schedule(el){ el.style.opacity = 1; }
|
||||
function reset(){
|
||||
t0 = performance.now(); arrow=0; failFlare=0; vecStall=0; causeGlow=0;
|
||||
verdict.style.opacity=0; wordmark.style.opacity=0; setCap('');
|
||||
seedBrain();
|
||||
}
|
||||
document.getElementById('replay').addEventListener('click', reset);
|
||||
|
||||
// ---- draw a soft additive glow blob ----
|
||||
function glow(x,y,rad,col,alpha){
|
||||
const g = ctx.createRadialGradient(x,y,0,x,y,rad);
|
||||
g.addColorStop(0, `rgba(${col[0]},${col[1]},${col[2]},${alpha})`);
|
||||
g.addColorStop(1, `rgba(${col[0]},${col[1]},${col[2]},0)`);
|
||||
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x,y,rad,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
|
||||
function frame(now){
|
||||
const t = (now - t0);
|
||||
if(t > DUR){ reset(); requestAnimationFrame(frame); return; }
|
||||
const pulse = 0.5 + 0.5*Math.sin(now*0.002); // ~0.32Hz breath
|
||||
|
||||
// ---- beat logic ----
|
||||
// 0.0-1.5s brain forms + settles; failure fades in ~0.3s
|
||||
// 4.0-6.5s vector search stalls on confounders
|
||||
// 6.5-10.5s backward trace fires
|
||||
// 10.5-13s verdict
|
||||
// 13-15s signature
|
||||
failFlare = t>300 ? Math.min(1,(t-300)/700) : 0;
|
||||
if(t>4000 && t<6500){ vecStall = Math.min(1,(t-4000)/600); setCap('vector search: finds what looks like the bug', true); }
|
||||
if(t>=1500 && t<4000){ setCap('your agent’s bug, today →', true); }
|
||||
if(t>=6500 && t<10500){
|
||||
arrow = Math.min(1,(t-6500)/3400);
|
||||
vecStall = Math.max(0, vecStall-0.02);
|
||||
setCap('Vestige reaches back to what caused it', true);
|
||||
}
|
||||
if(t>=9200){ causeGlow = Math.min(1,(t-9200)/900); }
|
||||
if(t>=10500 && t<13000){ setCap(''); verdict.style.opacity=1; }
|
||||
if(t>=13000){ wordmark.style.opacity=1; setCap('memory that finds the cause, not the resemblance', false); }
|
||||
|
||||
// field recedes behind the verdict so the headline is the focus
|
||||
let fieldDim = 1;
|
||||
if(t>=10200 && t<13000) fieldDim = 0.34;
|
||||
else if(t>=13000) fieldDim = 0.7;
|
||||
|
||||
// ---- render ----
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.fillStyle = 'rgba(5,6,10,0.30)'; // trails / motion blur
|
||||
ctx.fillRect(0,0,W,H);
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
|
||||
const formT = Math.min(1, t/1400);
|
||||
// update + draw particles
|
||||
for(let i=0;i<pts.length;i++){
|
||||
const p = pts[i];
|
||||
// spring to brain-home + gentle curl-ish drift
|
||||
const k = 0.02*formT;
|
||||
p.vx += (p.hx-p.x)*k; p.vy += (p.hy-p.y)*k;
|
||||
const ca = now*0.0006 + i*0.3;
|
||||
p.vx += Math.cos(ca+p.hy*0.01)*0.06; p.vy += Math.sin(ca+p.hx*0.01)*0.06;
|
||||
// breathing: tighten toward center on systole
|
||||
p.vx *= 0.90; p.vy *= 0.90;
|
||||
p.x += p.vx; p.y += p.vy;
|
||||
|
||||
// ignition decays
|
||||
p.ignite *= 0.94;
|
||||
|
||||
// hue: base + velocity-driven film shift + breath
|
||||
const speed = Math.hypot(p.vx,p.vy);
|
||||
let w = p.base + Math.min(speed*0.05,0.25) + 0.05*pulse + p.ignite*0.35;
|
||||
const col = spectral(w);
|
||||
// brighter, luminous tissue — floor keeps it from muddy, scale keeps it vivid
|
||||
let R = 30+col[0]*225, G = 40+col[1]*215, B = 90+col[2]*165;
|
||||
let a = (0.09 + 0.16*formT + p.ignite*0.7) * fieldDim;
|
||||
const rad = p.r*DPR*(0.85 + p.ignite*3);
|
||||
glow(p.x, p.y, rad*3.4, [R|0,G|0,B|0], a*0.45);
|
||||
ctx.fillStyle = `rgba(${R|0},${G|0},${B|0},${Math.min(1,a*1.9)})`;
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,rad,0,Math.PI*2); ctx.fill();
|
||||
}
|
||||
|
||||
// ---- failure flare (red) ----
|
||||
if(failFlare>0){
|
||||
const shake = failFlare<1 ? (Math.random()-0.5)*4*DPR : 0;
|
||||
glow(actors.fail.x+shake, actors.fail.y, 70*DPR*(0.7+0.3*pulse), [230,70,70], 0.5*failFlare);
|
||||
glow(actors.fail.x+shake, actors.fail.y, 24*DPR, [255,180,170], 0.8*failFlare);
|
||||
}
|
||||
|
||||
// ---- vector search: gray beams to confounders, then a fail cross ----
|
||||
if(vecStall>0){
|
||||
ctx.strokeStyle = `rgba(150,150,150,${0.5*vecStall})`;
|
||||
ctx.lineWidth = 1.2*DPR; ctx.setLineDash([5*DPR,4*DPR]);
|
||||
[actors.conf1, actors.conf2].forEach(c=>{
|
||||
ctx.beginPath(); ctx.moveTo(actors.fail.x,actors.fail.y); ctx.lineTo(c.x,c.y); ctx.stroke();
|
||||
});
|
||||
ctx.setLineDash([]);
|
||||
// faint pulses on the confounders it wrongly surfaces
|
||||
[actors.conf1, actors.conf2].forEach(c=> glow(c.x,c.y,26*DPR,[150,150,150],0.25*vecStall));
|
||||
}
|
||||
|
||||
// ---- the backward-trace arrow: bright iridescent light travelling fail -> cause ----
|
||||
if(arrow>0){
|
||||
const ax = actors.fail.x + (actors.cause.x-actors.fail.x)*arrow;
|
||||
const ay = actors.fail.y + (actors.cause.y-actors.fail.y)*arrow
|
||||
+ Math.sin(arrow*Math.PI)* -60*DPR; // arc up through the cortex
|
||||
// trailing comet
|
||||
for(let s=0;s<14;s++){
|
||||
const tt = Math.max(0, arrow - s*0.02);
|
||||
const tx = actors.fail.x + (actors.cause.x-actors.fail.x)*tt;
|
||||
const ty = actors.fail.y + (actors.cause.y-actors.fail.y)*tt + Math.sin(tt*Math.PI)*-60*DPR;
|
||||
glow(tx,ty, (10-s*0.5)*DPR, [90,230,210], 0.22*(1-s/14));
|
||||
}
|
||||
glow(ax,ay, 16*DPR, [140,245,255], 0.9);
|
||||
glow(ax,ay, 40*DPR, [90,220,255], 0.4);
|
||||
// ignite nearby particles as it passes (propagating thought)
|
||||
for(let i=0;i<pts.length;i++){
|
||||
const p=pts[i]; const d=Math.hypot(p.x-ax,p.y-ay);
|
||||
if(d < 55*DPR) p.ignite = Math.min(1, p.ignite + (1-d/(55*DPR))*0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- cause neuron ignites ----
|
||||
if(causeGlow>0){
|
||||
glow(actors.cause.x, actors.cause.y, 80*DPR*(0.7+0.3*pulse), [93,230,180], 0.55*causeGlow);
|
||||
glow(actors.cause.x, actors.cause.y, 26*DPR, [180,255,220], 0.9*causeGlow);
|
||||
// ripple ring
|
||||
const rr = (1-((t-9200)%1400/1400))*90*DPR;
|
||||
ctx.strokeStyle = `rgba(93,230,180,${0.4*causeGlow})`; ctx.lineWidth=2*DPR;
|
||||
ctx.beginPath(); ctx.arc(actors.cause.x,actors.cause.y, 90*DPR-rr, 0, Math.PI*2); ctx.stroke();
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue