mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-02 22:01:01 +02:00
feat(cinema): endless dream mode — infinite generative figures after the tour
The 7-beat tour no longer freezes on the last figure. When it ends, Memory Cinema drops into an infinite generative loop: every ~5.5s it morphs into a fresh RANDOM procedural figure and detonates a color blast — each crazier than the last. Five new procedural worlds (7..11), parameterized by a per-figure uMorphSeed + a uChaos ramp so the same index never looks the same twice: 7 supershape (3D superformula) 8 torus knot (random p,q winding) 9 warped lissajous lattice 10 helix storm 11 quantum foam (curl-warped chaos — max wild) storm.dreamBeat() picks a random world, reseeds it, ramps chaos, and fires a moderate-ignition blast (kept below the tour's 8.0 so dense random figures don't wash white). Surfaced via sandbox.dreamBeat(); MemoryCinema starts a dream timer on director onComplete, shows "∞ Dreaming", and tears it down on close/replay. Honors reduced-motion (no dream loop) and the render-fail fallback. Gate: svelte-check 0/0, 937/937 tests pass, build green, verified live (reaches dream mode, generates distinct figures — supershapes, torus knots — cycling forever, no white-out, no errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2422f5be6c
commit
ecb518bae8
3 changed files with 176 additions and 3 deletions
|
|
@ -70,6 +70,10 @@
|
|||
let lastFrame = 0;
|
||||
let typeTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let renderFailures = 0;
|
||||
// ENDLESS DREAM MODE — after the tour ends, fire a new random crazier figure
|
||||
// on this timer so the storm never sits idle. ($state so the template's
|
||||
// "∞ dreaming" indicator reacts when it starts/stops.)
|
||||
let dreamTimer = $state<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const reducedMotion =
|
||||
typeof window !== 'undefined' &&
|
||||
|
|
@ -173,6 +177,7 @@
|
|||
async function launch() {
|
||||
// Tear down any prior run so Replay never inherits stale state.
|
||||
cancelAnimationFrame(rafId);
|
||||
stopDreamMode();
|
||||
if (typeTimer) clearInterval(typeTimer);
|
||||
director?.stop();
|
||||
sandbox?.dispose();
|
||||
|
|
@ -246,7 +251,11 @@
|
|||
onProgress: (t) => (progress = t),
|
||||
onComplete: () => {
|
||||
stage = 'done';
|
||||
statusLine = 'End of tour.';
|
||||
statusLine =
|
||||
reducedMotion || !webgpuActive
|
||||
? 'End of tour.'
|
||||
: '∞ Dreaming — endless generative figures';
|
||||
startDreamMode();
|
||||
},
|
||||
}, { reducedMotion, shots, centerOnOrigin: webgpuActive });
|
||||
|
||||
|
|
@ -288,8 +297,40 @@
|
|||
}
|
||||
}
|
||||
|
||||
// ── ENDLESS DREAM MODE ──────────────────────────────────────────────────
|
||||
// When the scripted tour ends, instead of freezing on the last figure, the
|
||||
// storm enters an infinite generative loop: every few seconds it morphs into
|
||||
// a fresh RANDOM procedural figure (supershape, torus-knot, lissajous, helix,
|
||||
// quantum foam) and detonates a color blast — each one crazier than the last.
|
||||
// The render loop() is already running, so we just fire dreamBeats on a timer.
|
||||
function startDreamMode() {
|
||||
if (reducedMotion || !sandbox || !webgpuActive) return; // honor reduced-motion
|
||||
stopDreamMode();
|
||||
// Fire the first wild figure immediately, then keep going forever.
|
||||
sandbox?.dreamBeat();
|
||||
caption = '';
|
||||
chip = 'Dreaming';
|
||||
dreamTimer = setInterval(() => {
|
||||
// Sandbox may have been torn down (close / render-fail fallback).
|
||||
if (!sandbox || !webgpuActive) {
|
||||
stopDreamMode();
|
||||
return;
|
||||
}
|
||||
sandbox.dreamBeat();
|
||||
}, 5500); // a beat every ~5.5s — the blast flares then the figure settles
|
||||
// into its clean shape before the next detonation.
|
||||
}
|
||||
|
||||
function stopDreamMode() {
|
||||
if (dreamTimer) {
|
||||
clearInterval(dreamTimer);
|
||||
dreamTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
cancelAnimationFrame(rafId);
|
||||
stopDreamMode();
|
||||
if (typeTimer) clearInterval(typeTimer);
|
||||
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel();
|
||||
director?.stop();
|
||||
|
|
@ -435,7 +476,8 @@
|
|||
></div>
|
||||
</div>
|
||||
<div class="cinema-beatcount text-dim text-xs">
|
||||
{#if totalBeats > 0}Beat {beatIndex} / {totalBeats}{/if}
|
||||
{#if stage === 'done' && dreamTimer}<span class="cinema-dream">∞ dreaming</span>
|
||||
{:else if totalBeats > 0}Beat {beatIndex} / {totalBeats}{/if}
|
||||
{#if stage === 'done'}<button class="cinema-replay" onclick={launch}>↻ Replay</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -602,6 +644,15 @@
|
|||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.cinema-dream {
|
||||
color: var(--color-dream-glow);
|
||||
letter-spacing: 0.08em;
|
||||
animation: cinema-dream-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes cinema-dream-pulse {
|
||||
0%, 100% { opacity: 0.55; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cinema-progress-fill {
|
||||
transition: none;
|
||||
|
|
|
|||
|
|
@ -178,6 +178,13 @@ export class CinemaSandbox {
|
|||
this.storm.transitionTo(role, ORIGIN, act, beatIndex);
|
||||
}
|
||||
|
||||
/** Fire one endless-dream beat — a random crazier figure + color blast. Called
|
||||
* on a timer after the scripted tour ends so the storm never sits idle. */
|
||||
dreamBeat(): void {
|
||||
if (!this.booted) return;
|
||||
this.storm.dreamBeat();
|
||||
}
|
||||
|
||||
/** Render one frame. The storm is pinned to the origin and the camera always
|
||||
* looks at the origin, so the storm CANNOT leave the frame. The director
|
||||
* varies only the camera's orbital position/angle (set via cameraRef), and we
|
||||
|
|
|
|||
|
|
@ -137,6 +137,14 @@ export class SemanticComputeStorm {
|
|||
// seconds since the last detonation and drives the outward spectral wave.
|
||||
private uBlast = uniform(0);
|
||||
private uBlastTime = uniform(0);
|
||||
// ENDLESS DREAM MODE — after the scripted 7-beat tour, the storm keeps
|
||||
// generating crazier figures forever instead of sitting idle. uMorphSeed
|
||||
// randomizes each procedural figure (worlds 7..11); uChaos ramps 0→1 over the
|
||||
// dream so every figure is wilder than the last.
|
||||
private uMorphSeed = uniform(0);
|
||||
private uChaos = uniform(0);
|
||||
// JS-side dream state (not uniforms): which figure is live + how many fired.
|
||||
private dreamCount = 0;
|
||||
|
||||
constructor(
|
||||
renderer: { computeAsync: (node: ComputeDispatch) => Promise<void> },
|
||||
|
|
@ -252,6 +260,74 @@ export class SemanticComputeStorm {
|
|||
const pRad = sqrt(fi).mul(R.mul(0.0042)); // ~R at 150k particles
|
||||
const wPhyllo = vec3(pAng.cos().mul(pRad), R.mul(0.04).mul(sin(phase.mul(9))), pAng.sin().mul(pRad));
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// ENDLESS DREAM FIGURES (worlds 7..11) — the generative mode that
|
||||
// kicks in after the scripted 7-beat tour. These are PROCEDURAL and
|
||||
// RANDOMIZED: uMorphSeed (set per auto-beat) + uChaos (ramps up over
|
||||
// time → each figure crazier than the last) modulate the parameters,
|
||||
// so the same world index never looks the same twice.
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
const seed = this.uMorphSeed;
|
||||
const chaos = this.uChaos;
|
||||
// seeded per-figure scalars (deterministic hash of the seed)
|
||||
const s1 = fract(seed.mul(0.731).add(0.13));
|
||||
const s2 = fract(seed.mul(1.323).add(0.51));
|
||||
const s3 = fract(seed.mul(2.117).add(0.27));
|
||||
|
||||
// world 7 · SUPERSHAPE (3D superformula — petals/stars/blobs, never same)
|
||||
const m1 = float(2).add(floor(s1.mul(14))); // symmetry 2..15
|
||||
const sfAng = theta;
|
||||
const sfR1 = pow(abs(cos(m1.mul(sfAng).div(4))), float(2).add(s2.mul(8)))
|
||||
.add(pow(abs(sin(m1.mul(sfAng).div(4))), float(2).add(s3.mul(8))))
|
||||
.add(0.0001)
|
||||
.pow(float(-0.5));
|
||||
const sfR2 = pow(abs(cos(m1.mul(phi).div(4))), float(3))
|
||||
.add(pow(abs(sin(m1.mul(phi).div(4))), float(3)))
|
||||
.add(0.0001)
|
||||
.pow(float(-0.5));
|
||||
const sfRad = R.mul(0.85).mul(clamp(sfR1.mul(sfR2).mul(0.5), 0.1, 1.4));
|
||||
const wSuper = vec3(
|
||||
sin(phi).mul(cos(theta)).mul(sfRad),
|
||||
cos(phi).mul(sfRad),
|
||||
sin(phi).mul(sin(theta)).mul(sfRad)
|
||||
);
|
||||
|
||||
// world 8 · TORUS KNOT (p,q knot — randomized winding, hypnotic ribbons)
|
||||
const pKnot = float(2).add(floor(s1.mul(5))); // 2..6
|
||||
const qKnot = float(3).add(floor(s2.mul(5))); // 3..7
|
||||
const kt = fi.mul(0.0006).add(this.uTime.mul(0.1));
|
||||
const kr = cos(qKnot.mul(kt)).mul(0.4).add(1);
|
||||
const wKnot = vec3(
|
||||
kr.mul(cos(pKnot.mul(kt))),
|
||||
kr.mul(sin(pKnot.mul(kt))),
|
||||
sin(qKnot.mul(kt)).mul(0.55)
|
||||
).mul(R.mul(0.6)).add(sphereShell.mul(R.mul(0.06))); // slight fuzz
|
||||
|
||||
// world 9 · WARPED LISSAJOUS LATTICE (3D sine-wave interference web)
|
||||
const fx = float(2).add(floor(s1.mul(5)));
|
||||
const fy = float(2).add(floor(s2.mul(5)));
|
||||
const fz = float(2).add(floor(s3.mul(5)));
|
||||
const lt = fi.mul(0.0007);
|
||||
const wLissa = vec3(
|
||||
sin(fx.mul(lt).add(this.uTime.mul(0.3))),
|
||||
sin(fy.mul(lt).add(1.7)),
|
||||
sin(fz.mul(lt).add(this.uTime.mul(0.2)).add(3.1))
|
||||
).mul(R.mul(0.82));
|
||||
|
||||
// world 10 · HELIX STORM (twisted DNA-ish double helix that writhes)
|
||||
const hAng = fi.mul(0.0009).add(this.uTime.mul(0.4));
|
||||
const hSide = select(fract(phase.mul(2)).greaterThan(0.5), float(1), float(-1));
|
||||
const hRad = R.mul(0.55).mul(float(0.7).add(sin(hAng.mul(3)).mul(0.3).mul(chaos.add(0.3))));
|
||||
const wHelix = vec3(
|
||||
cos(hAng).mul(hRad).mul(hSide),
|
||||
fi.mul(0.00026).sub(R.mul(0.9)).mul(0.5).add(sin(this.uTime).mul(R.mul(0.1))),
|
||||
sin(hAng).mul(hRad).mul(hSide)
|
||||
);
|
||||
|
||||
// world 11 · QUANTUM FOAM (curl-warped noisy blob — pure chaos, max wild)
|
||||
const foam = mx_noise_vec3(sphereShell.mul(float(1.5).add(chaos.mul(3))).add(seed)).mul(R.mul(0.5).mul(chaos.add(0.4)));
|
||||
const wFoam = sphereShell.mul(R.mul(homeFrac)).add(foam);
|
||||
|
||||
// select() chain — no dynamic indexing in this TSL build.
|
||||
const homeFor = (idx: ReturnType<typeof float>) =>
|
||||
select(idx.equal(0), wNebula,
|
||||
|
|
@ -259,7 +335,12 @@ export class SemanticComputeStorm {
|
|||
select(idx.equal(2), wAttractor,
|
||||
select(idx.equal(3), wVoid,
|
||||
select(idx.equal(4), wCrystal,
|
||||
select(idx.equal(5), wGalaxy, wPhyllo))))));
|
||||
select(idx.equal(5), wGalaxy,
|
||||
select(idx.equal(6), wPhyllo,
|
||||
select(idx.equal(7), wSuper,
|
||||
select(idx.equal(8), wKnot,
|
||||
select(idx.equal(9), wLissa,
|
||||
select(idx.equal(10), wHelix, wFoam)))))))))));
|
||||
const homeCur = homeFor(float(this.uWorld));
|
||||
const homePrev = homeFor(float(this.uPrevWorld));
|
||||
// uBlend eases prev→cur (smoothstep) so the world morph is silky.
|
||||
|
|
@ -570,6 +651,40 @@ export class SemanticComputeStorm {
|
|||
this.uModeTintAmt.value = mode >= 2 ? 0.7 : 0.22;
|
||||
}
|
||||
|
||||
/**
|
||||
* ENDLESS DREAM BEAT — fired on a timer AFTER the scripted tour ends, so the
|
||||
* storm never sits idle. Jumps to a RANDOM procedural figure (worlds 7..11),
|
||||
* reseeds it (so it's never the same shape twice), ramps uChaos up so each one
|
||||
* is wilder than the last, and detonates a full color blast. This is the
|
||||
* "random figure generator that makes even crazier beats."
|
||||
*/
|
||||
dreamBeat(): void {
|
||||
this.dreamCount += 1;
|
||||
// Pick a random wild figure (worlds 7..11 are the procedural generators).
|
||||
const world = 7 + Math.floor(Math.random() * 5);
|
||||
this.uPrevWorld.value = this.uWorld.value;
|
||||
this.uWorld.value = world;
|
||||
this.uBlend.value = 1;
|
||||
// Fresh random seed → the superformula/knot/lissajous/helix/foam params all
|
||||
// change, so the same world index never looks the same twice.
|
||||
this.uMorphSeed.value = Math.random() * 1000;
|
||||
// Chaos ramps up and saturates — figures get progressively crazier, then
|
||||
// hold at max wildness. Eases in over the first ~8 dream beats.
|
||||
this.uChaos.value = Math.min(1, 0.25 + this.dreamCount * 0.1);
|
||||
// Detonation + long color blast every dream beat — but ignition kept
|
||||
// MODERATE (not the tour's 8.0) so the random dense figures don't wash to
|
||||
// white at the blast peak. The rim-gated spectral blast carries the color.
|
||||
this.uActDim.value = 0.85;
|
||||
this.uIgnition.value = 3.0;
|
||||
this.uBurst.value = 1.0;
|
||||
this.uBlast.value = 1.0;
|
||||
this.uBlastTime.value = 0;
|
||||
// Vary the mode tint randomly too so the palette keeps surprising.
|
||||
const modes = [1, 2, 3];
|
||||
this.uMode.value = modes[Math.floor(Math.random() * modes.length)];
|
||||
this.uModeTintAmt.value = 0.3 + Math.random() * 0.5;
|
||||
}
|
||||
|
||||
/** Size the containment sphere (world units) so the storm always stays in
|
||||
* frame. The sandbox derives this from the camera distance + fov. */
|
||||
setContainRadius(radius: number): void {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue