From 5c3fe1f4a20b8564ca623cef5003a8a9e445bd6d Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 25 Sep 2026 11:08:22 -0700 Subject: [PATCH] Let the wandering camera follow the biggest school as it swims, favour it at night when its wake glows, watch the largest structure through a storm, and not show the school twice running. --- public/tide-pool.js | 34 ++++++++++++++++++++++++++++++---- src/components/tide-pool.tsx | 2 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/public/tide-pool.js b/public/tide-pool.js index 06beb28..4607721 100644 --- a/public/tide-pool.js +++ b/public/tide-pool.js @@ -1214,7 +1214,12 @@ async function initialize() { const WEIGHT = { collapse: 9, fall: 6, form: 5, found: 5, debut: 4, erect: 3.5, complete: 3, birth: 3, arrive: 2.5, repair: 2 }; function candidates() { const light = daylight(world), picks = []; - const add = (x, z, score, zoom) => { if (Number.isFinite(x + z) && onShelf(world, x, z)) picks.push({ x, z, score, zoom }); }; + const add = (x, z, score, zoom) => { + if (!Number.isFinite(x + z) || !onShelf(world, x, z)) return null; + const pick = { x, z, score, zoom }; + picks.push(pick); + return pick; + }; for (const e of world.events) { const age = world.time - e.time; if (age > 45 || e.x == null || !WEIGHT[e.type]) continue; @@ -1229,16 +1234,32 @@ async function initialize() { } add(t.x, t.z, 1 + t.n / 70 + lights * (.4 + dusk), clamp(3 + t.reach, 3.4, 6)); } - const fish = world.creatures.filter(c => c.sp === 'tab'); - if (fish.length > 4) add(fish.reduce((a, c) => a + c.x, 0) / fish.length, fish.reduce((a, c) => a + c.z, 0) / fish.length, 1.4, 3.4); + // The school, where the most fish swim together; at night its glowing wake is the best thing on the shelf. + const school = schoolAt(); + const followed = school && school.n > 3 && add(school.x, school.z, 1 + school.n * .1 + dark * 3, 3.2); + if (followed) followed.school = true; + // In a storm, the largest structure taking the waves. + const storm = stormAt(world); + if (storm > .1 && world.structures.length) { const t = [...world.structures].sort((a, b) => b.n - a.n)[0]; add(t.x, t.z, 4 + storm * 4, 4.2); } return picks; } + function schoolAt() { + const fish = world.creatures.filter(c => c.sp === 'tab'); + let best = null; + for (const a of fish) { + const near = fish.filter(b => Math.hypot(a.x - b.x, a.z - b.z) < 2); + if (!best || near.length > best.length) best = near; + } + return best && { x: best.reduce((s, c) => s + c.x, 0) / best.length, z: best.reduce((s, c) => s + c.z, 0) / best.length, n: best.length }; + } function direct(dt) { const now = performance.now(), last = director.shot; const urgent = last && world.events.some(e => (e.type === 'collapse' || e.type === 'fall') && world.time - e.time < 2 && e.x != null && Math.hypot(e.x - last.x, e.z - last.z) > 1); if (!last || now > director.until || urgent) { - const picks = candidates().map(p => ({ ...p, score: p.score + Math.random() * .8 - (last && Math.hypot(p.x - last.x, p.z - last.z) < 2 ? 1.5 : 0) })); + // Something else next time: not the same place, and not the school twice running. + const picks = candidates().map(p => ({ ...p, score: p.score + Math.random() * .8 - (last && Math.hypot(p.x - last.x, p.z - last.z) < 2 ? 1.5 : 0) - + (last?.school && p.school ? 3 : 0) })); picks.sort((a, b) => b.score - a.score); // Now and then a wide shot of the whole shelf between the close ones. director.wide = !urgent && !director.wide && Math.random() < .3; @@ -1247,6 +1268,11 @@ async function initialize() { director.until = now + (director.wide ? 14000 : 20000 + Math.random() * 12000); } director.phase += dt; + // A school swims on, so a shot of one keeps it in view. + if (director.shot.school) { + const school = schoolAt(); + if (school) { director.shot.x += (school.x - director.shot.x) * Math.min(1, dt * .4); director.shot.z += (school.z - director.shot.z) * Math.min(1, dt * .4); } + } // Never quite still: a slow circle around the subject, and a slow breath in and out. const shot = director.shot, t = director.phase; goal.x = shot.x + Math.cos(t * .07) * .7; goal.z = shot.z + Math.sin(t * .053) * .45; diff --git a/src/components/tide-pool.tsx b/src/components/tide-pool.tsx index 5878bcd..64350a5 100644 --- a/src/components/tide-pool.tsx +++ b/src/components/tide-pool.tsx @@ -141,7 +141,7 @@ export function TidePoolContent() { {/* Lets the three.js add-ons resolve the same pinned module the page already uses. */} + ); } -- 2.51.2