Leaves: a pooled handful (cap 7) streaming with the wind from 8.3 m/s — the same number D keys the player's lean on, so the yard and the body start telling one story at one speed. Count rides a 2.5 s speed EMA so it doesn't strobe; own seeded rng (seed ^ 0x1eaf) so the pool never shifts the crate sequence; bit-identical under identical (dt, t) streams (asserted). debris.clear() rewinds the layer. dev_skyfx flies the debris layer now and warms it to the scrub point, so ?t=50 shows the leaves t=50 would have. D's aftermath sliver, exactly as diagnosed on lane/d: the hail InstancedMesh at count:0 with frustum culling and depthWrite off still draws instance 0's identity matrix — a 5 cm always-on-top speck at the origin. mesh.visible now gates on n > 0, sky.hail joins the public fx object, and a whole-storm assert demands hidden-at-zero / shown-when-falling in both directions (gated on floor(1300 x intensity) >= 1: a burst's first frames honestly draw nothing). THREADS: gate-2 wrap entry — D's judging unblocked, A's M key live. Selftest 340/0/0 (+2 tests). Mutation-checked: dropped stepLeaves, a 3 m/s threshold, and an always-visible hail pool all go red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
109 lines
4.8 KiB
HTML
109 lines
4.8 KiB
HTML
<!doctype html>
|
||
<!--
|
||
dev_skyfx — Lane C's storm viewer. [SPRINT12]
|
||
|
||
The skyfx bench: one storm, a bare ground plane, and the whole sky stack
|
||
(dome, rain, hail, lightning, change front, audio) with a scrubbable clock.
|
||
Exists so "does the change ANNOUNCE itself" (gate 3.4) can be judged by eye
|
||
in ten seconds instead of playing to night 3 — D, this is for you: load the
|
||
early buster, watch the southern sky from t=0, and say whether you'd have
|
||
known the change was coming without reading a number.
|
||
|
||
dev_skyfx.html ← storm_03b_earlybuster
|
||
dev_skyfx.html?storm=storm_03_southerly&t=12 ← night 2's slower version
|
||
|
||
keys: space pause · ] +5 s · [ −5 s (forward-clean; rewinds may re-flash)
|
||
←/→ turn · click once to unlock audio
|
||
The VIEW runs on rAF (it's a viewer); the sim is stepped at FIXED_DT
|
||
accumulated, same as the game, so what you see is what ships.
|
||
-->
|
||
<meta charset="utf-8">
|
||
<title>dev_skyfx</title>
|
||
<style>
|
||
body { margin:0; background:#000; overflow:hidden; }
|
||
#hud { position:fixed; top:10px; left:12px; color:#cde; font:12px/1.5 ui-monospace,Menlo,monospace;
|
||
text-shadow:0 1px 2px #000; white-space:pre; pointer-events:none; }
|
||
</style>
|
||
<div id="hud">loading…</div>
|
||
<script type="importmap">
|
||
{ "imports": { "three": "./vendor/three.module.js",
|
||
"three/addons/": "./vendor/addons/" } }
|
||
</script>
|
||
<script type="module">
|
||
import * as THREE from './vendor/three.module.js';
|
||
import { FIXED_DT } from './js/contracts.js';
|
||
import { loadStorm, createWind } from './js/weather.js';
|
||
import { createSkyFx } from './js/skyfx.js';
|
||
import { createDebris } from './js/debris.js';
|
||
|
||
const q = new URLSearchParams(location.search);
|
||
const stormName = q.get('storm') || 'storm_03b_earlybuster';
|
||
const def = await loadStorm(stormName);
|
||
const wind = createWind(def);
|
||
|
||
const scene = new THREE.Scene();
|
||
scene.background = new THREE.Color(0x9fc4e8);
|
||
const camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 400);
|
||
camera.position.set(0, 1.7, -4);
|
||
let yaw = Math.PI; // start looking toward +Z — the south
|
||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||
renderer.setSize(innerWidth, innerHeight);
|
||
document.body.appendChild(renderer.domElement);
|
||
|
||
const sun = new THREE.DirectionalLight(0xfff4e0, 2.0);
|
||
sun.position.set(30, 50, -20);
|
||
scene.add(sun);
|
||
const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x3a4a2a, 1.2);
|
||
scene.add(hemi);
|
||
const ground = new THREE.Mesh(
|
||
new THREE.CircleGeometry(120, 48).rotateX(-Math.PI / 2),
|
||
new THREE.MeshLambertMaterial({ color: 0x4a5d3a }),
|
||
);
|
||
scene.add(ground);
|
||
// a fence-height slab to the north, so there's a yard-ish thing in frame
|
||
const slab = new THREE.Mesh(new THREE.BoxGeometry(14, 3, 1), new THREE.MeshLambertMaterial({ color: 0x8a7f70 }));
|
||
slab.position.set(0, 1.5, -11);
|
||
scene.add(slab);
|
||
|
||
const sky = createSkyFx({ scene, camera, wind, sun, hemi });
|
||
// gate 2.3: the viewer flies the ambient-leaf layer too — the sky stack and
|
||
// the yard's tell belong in the same eyeball test
|
||
const debris = createDebris({ wind, scene });
|
||
addEventListener('pointerdown', () => sky.unlockAudio(), { once: false });
|
||
|
||
let t = Math.max(0, +q.get('t') || 0), paused = false, last = performance.now(), acc = 0;
|
||
// Warm the ambient layer to the scrub point: leaves carry a few seconds of
|
||
// state (speed EMA + travel), so a viewer that spawns them cold at ?t=50
|
||
// shows an empty sky the real t=50 would not.
|
||
for (let wt = Math.max(0, t - 8); wt < t; wt += FIXED_DT) debris.step(FIXED_DT, wt, {});
|
||
addEventListener('keydown', (e) => {
|
||
if (e.code === 'Space') paused = !paused;
|
||
if (e.key === ']') t = Math.min(def.duration, t + 5);
|
||
if (e.key === '[') t = Math.max(0, t - 5);
|
||
if (e.key === 'ArrowLeft') yaw += 0.15;
|
||
if (e.key === 'ArrowRight') yaw -= 0.15;
|
||
});
|
||
addEventListener('resize', () => {
|
||
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
|
||
renderer.setSize(innerWidth, innerHeight);
|
||
});
|
||
|
||
const hud = document.getElementById('hud');
|
||
function frame(now) {
|
||
requestAnimationFrame(frame);
|
||
const wall = Math.min(0.1, (now - last) / 1000); last = now;
|
||
if (!paused && t < def.duration) {
|
||
acc += wall;
|
||
while (acc >= FIXED_DT) { acc -= FIXED_DT; t += FIXED_DT; sky.step(FIXED_DT, t, {}); debris.step(FIXED_DT, t, {}); }
|
||
}
|
||
camera.rotation.set(0, yaw, 0, 'YXZ');
|
||
const ch = (def.events || []).find((e) => e.type === 'windchange');
|
||
hud.textContent = `${stormName} t=${t.toFixed(1)}s / ${def.duration}s${paused ? ' ⏸' : ''}\n`
|
||
+ `wind ${wind.speedAt(camera.position, t).toFixed(1)} m/s rain ${wind.rainAt(t).toFixed(2)} `
|
||
+ `front ${sky.changeFront.toFixed(2)} leaves ${debris.leafCount}${ch ? ` (change at t=${ch.t})` : ' (no change)'}\n`
|
||
+ `space pause · [ ] scrub · ←→ turn · click for audio`;
|
||
renderer.render(scene, camera);
|
||
}
|
||
requestAnimationFrame(frame);
|
||
</script>
|