HardYards/web/world/dev_skyfx.html
type-two e5c0e3ffee skyfx: the change announces itself — a front wall in the sky (gate 3.4)
Every windchange event gets an in-world tell: a dark bank standing on the
horizon in the quarter the new wind comes FROM, rising across the 12 s
before the change and clearing after the swing, plus a low distant rumble
layer in the audio. Soft-edged via vertex alpha (the hard band read as a
floating rectangle on the bench). Deterministic off (dt, t): progress is a
pure function of t, azimuth comes from dirAt at a fixed post-change
instant. Night 2 teaches the tell at ~18 s; night 3 shows the same wall at
t=6, which is 'looks like night 2 and isn't' made visible.

dev_skyfx.html: the skyfx bench — one storm, bare ground, scrubbable
clock, so the tell can be judged by eye without playing to night 3.
Two new asserts in c.test.js (window, monotonic rise, direction, mesh
orientation, changeless storm stays clear, bit-exact determinism).
Selftest 322/0/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:24:51 +10:00

101 lines
4.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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';
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 });
addEventListener('pointerdown', () => sky.unlockAudio(), { once: false });
let t = Math.max(0, +q.get('t') || 0), paused = false, last = performance.now(), acc = 0;
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, {}); }
}
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)}${ch ? ` (change at t=${ch.t})` : ' (no change)'}\n`
+ `space pause · [ ] scrub · ←→ turn · click for audio`;
renderer.render(scene, camera);
}
requestAnimationFrame(frame);
</script>