@idle-screens/saver-metaquarium 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1620,7 +1620,7 @@ function shardGeometry(rng) {
1620
1620
  for (let i = 0; i < n; i += 1) tris.push([top[i], tip, top[(i + 1) % n]]);
1621
1621
  const positions = new Float32Array(tris.length * 9);
1622
1622
  const normals = new Float32Array(tris.length * 9);
1623
- const smooth3 = new Float32Array(tris.length * 9);
1623
+ const smooth4 = new Float32Array(tris.length * 9);
1624
1624
  tris.forEach((tri, t) => {
1625
1625
  const [a, b, c] = tri;
1626
1626
  const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
@@ -1648,12 +1648,12 @@ function shardGeometry(rng) {
1648
1648
  normals[o + 2] = nz;
1649
1649
  const sy = (p[1] - 0.55) * 0.25;
1650
1650
  const sl = Math.hypot(p[0], sy, p[2]) || 1;
1651
- smooth3[o] = p[0] / sl;
1652
- smooth3[o + 1] = sy / sl;
1653
- smooth3[o + 2] = p[2] / sl;
1651
+ smooth4[o] = p[0] / sl;
1652
+ smooth4[o + 1] = sy / sl;
1653
+ smooth4[o + 2] = p[2] / sl;
1654
1654
  });
1655
1655
  });
1656
- return { positions, normals, smooth: smooth3, triangles: tris.length };
1656
+ return { positions, normals, smooth: smooth4, triangles: tris.length };
1657
1657
  }
1658
1658
  var HABIT_RINGS = {
1659
1659
  lotus: [[1, 0], [6, 30], [9, 55], [16, 85]],
@@ -2500,10 +2500,196 @@ function parseSignMix(value) {
2500
2500
  return { entries, problems };
2501
2501
  }
2502
2502
 
2503
+ // src/choreo.ts
2504
+ var STARFISH_MOVE_NAMES = [
2505
+ "march",
2506
+ "jacks",
2507
+ "reach",
2508
+ "kick",
2509
+ "twist",
2510
+ "circles",
2511
+ "disco",
2512
+ "spin",
2513
+ "mambo",
2514
+ "sway",
2515
+ "twirl",
2516
+ "dip",
2517
+ "hold",
2518
+ "walk",
2519
+ "lie",
2520
+ "crawl",
2521
+ "curl",
2522
+ "wave",
2523
+ "pop",
2524
+ "rise",
2525
+ "sit"
2526
+ ];
2527
+ var CHOREO_MARKS = {
2528
+ home: [0, 0],
2529
+ left: [-2, 0],
2530
+ right: [2, 0],
2531
+ front: [0, 1.2],
2532
+ back: [0, -1.5],
2533
+ centre: "centre"
2534
+ };
2535
+ var MAX_CUES = 256;
2536
+ function parseChoreography(value, moves) {
2537
+ const lines = /* @__PURE__ */ new Map(), problems = [];
2538
+ const known = new Set(moves);
2539
+ for (const rawLine of value.split("|").map((s) => s.trim()).filter(Boolean)) {
2540
+ const head = /^([a-z]|\*)\s*:\s*(?=\d|\.)/.exec(rawLine);
2541
+ const actor = head ? head[1] === "*" ? "*" : head[1].charCodeAt(0) - 97 : "*";
2542
+ const body = head ? rawLine.slice(head[0].length) : rawLine;
2543
+ if (lines.has(actor)) problems.push(`dancer ${actor === "*" ? "*" : String.fromCharCode(97 + actor)} has two lines; the later one wins`);
2544
+ const cues = [];
2545
+ let b0 = 0;
2546
+ for (const raw of body.split(/,\s*(?=(?:\d+(?:\.\d+)?|\.\d+)\s*:)/).map((s) => s.trim()).filter(Boolean)) {
2547
+ if (cues.length >= MAX_CUES) {
2548
+ problems.push(`more than ${MAX_CUES} cues on one line; the rest are dropped`);
2549
+ break;
2550
+ }
2551
+ const m = /^(\d+(?:\.\d+)?|\.\d+)\s*:\s*([a-z][a-z_]*)([^a-z_].*)?$/.exec(raw);
2552
+ if (!m) {
2553
+ problems.push(`"${raw}" is not <beats>:<move>`);
2554
+ continue;
2555
+ }
2556
+ const beats = Number(m[1]);
2557
+ if (!(beats > 0)) {
2558
+ problems.push(`"${raw}": a cue needs some beats`);
2559
+ continue;
2560
+ }
2561
+ if (!known.has(m[2])) {
2562
+ problems.push(`"${raw}": no move "${m[2]}" (moves: ${moves.join(" ")})`);
2563
+ continue;
2564
+ }
2565
+ const cue = { b0, beats: Math.min(beats, 512), move: m[2], turn: 0, lift: 0 };
2566
+ for (const tok of (m[3] ?? "").replace(/^[,\s]+/, "").split(/\s+/).filter(Boolean)) {
2567
+ const mod = /^([>@~^/%*x=])(.+)$/.exec(tok);
2568
+ if (!mod) {
2569
+ problems.push(`"${raw}": "${tok}" is not >to, @face, ~turn, ^leap, /lean, %crouch, *groove, xspeed or =pose`);
2570
+ continue;
2571
+ }
2572
+ const [, op, arg] = mod;
2573
+ if (op === ">") {
2574
+ const mark = CHOREO_MARKS[arg];
2575
+ const xy = /^(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)$/.exec(arg);
2576
+ if (mark) cue.to = mark;
2577
+ else if (xy) cue.to = [clamp(Number(xy[1]), -6, 6), clamp(Number(xy[2]), -6, 6)];
2578
+ else problems.push(`"${raw}": travel to "${arg}" \u2014 a mark (${Object.keys(CHOREO_MARKS).join(" ")}) or side,depth`);
2579
+ } else if (op === "@") {
2580
+ const named = { cam: "cam", room: 0, front: 0, back: 180, left: -90, right: 90 };
2581
+ if (arg in named) cue.face = named[arg];
2582
+ else if (Number.isFinite(Number(arg))) cue.face = Number(arg);
2583
+ else problems.push(`"${raw}": face "${arg}" \u2014 degrees, cam, room, left, right or back`);
2584
+ } else if (op === "/") {
2585
+ if (Number.isFinite(Number(arg))) cue.lean = clamp(Number(arg), -60, 60);
2586
+ else problems.push(`"${raw}": lean "${arg}" is not degrees`);
2587
+ } else if (op === "%") {
2588
+ if (Number.isFinite(Number(arg)) && Number(arg) > 0) cue.squash = clamp(Number(arg), 0.4, 1.5);
2589
+ else problems.push(`"${raw}": crouch "${arg}" is not a height scale`);
2590
+ } else if (op === "=") {
2591
+ if (arg !== "" && Number.isFinite(Number(arg)) && Number(arg) >= 0 && Number(arg) <= 1) cue.pose = Number(arg);
2592
+ else problems.push(`"${raw}": pose "${arg}" is not a fraction of the move (0\u20131)`);
2593
+ } else if (op === "x") {
2594
+ if (Number.isFinite(Number(arg)) && Number(arg) > 0) cue.rate = clamp(Number(arg), 0.25, 4);
2595
+ else problems.push(`"${raw}": speed "${arg}" is not a multiple`);
2596
+ } else if (op === "*") {
2597
+ if (Number.isFinite(Number(arg)) && Number(arg) > 0) cue.groove = clamp(Number(arg), 0.02, 0.3);
2598
+ else problems.push(`"${raw}": groove "${arg}" is not a depth`);
2599
+ } else if (op === "~") {
2600
+ if (Number.isFinite(Number(arg))) cue.turn = clamp(Number(arg), -1440, 1440);
2601
+ else problems.push(`"${raw}": turn "${arg}" is not degrees`);
2602
+ } else {
2603
+ if (Number.isFinite(Number(arg)) && Number(arg) >= 0) cue.lift = Math.min(Number(arg), 3);
2604
+ else problems.push(`"${raw}": leap "${arg}" is not a height`);
2605
+ }
2606
+ }
2607
+ cues.push(cue);
2608
+ b0 += cue.beats;
2609
+ }
2610
+ if (cues.length) lines.set(actor, { cues, beats: b0 });
2611
+ else problems.push(`line "${rawLine}" has no cues`);
2612
+ }
2613
+ return { lines, problems };
2614
+ }
2615
+ var CHOREO_HANDOVER = 0.5;
2616
+ var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
2617
+ var smooth3 = (u) => {
2618
+ const c = clamp(u, 0, 1);
2619
+ return c * c * (3 - 2 * c);
2620
+ };
2621
+ var posMod = (v, m) => (v % m + m) % m;
2622
+ function choreoLine(sheet, slot) {
2623
+ return sheet?.lines.get(slot) ?? sheet?.lines.get("*") ?? null;
2624
+ }
2625
+ function choreoAt(line, beats) {
2626
+ const b = posMod(beats, line.beats);
2627
+ const n = line.cues.length;
2628
+ let i = 0;
2629
+ while (i < n - 1 && b >= line.cues[i].b0 + line.cues[i].beats) i++;
2630
+ const cue = line.cues[i], next = line.cues[(i + 1) % n], prev = line.cues[(i + n - 1) % n];
2631
+ let side = 0, depth = 0, centre = 0, face = 0, faceCam = 0, turn = 0;
2632
+ const end = (c) => {
2633
+ if (c.to === "centre") centre = 1;
2634
+ else if (c.to) {
2635
+ side = c.to[0];
2636
+ depth = c.to[1];
2637
+ centre = 0;
2638
+ }
2639
+ turn += c.turn;
2640
+ };
2641
+ const facing = (c) => {
2642
+ if (c.face === "cam") faceCam = 1;
2643
+ else if (c.face !== void 0) {
2644
+ face = c.face;
2645
+ faceCam = 0;
2646
+ }
2647
+ };
2648
+ for (let k = 0; k < i; k++) {
2649
+ end(line.cues[k]);
2650
+ facing(line.cues[k]);
2651
+ }
2652
+ facing(cue);
2653
+ const beat = b - cue.b0, u = clamp(beat / cue.beats, 0, 1);
2654
+ const s = smooth3(u);
2655
+ if (cue.to === "centre") centre += (1 - centre) * s;
2656
+ else if (cue.to) {
2657
+ side += (cue.to[0] - side) * s;
2658
+ depth += (cue.to[1] - depth) * s;
2659
+ centre *= 1 - s;
2660
+ }
2661
+ turn += cue.turn * s;
2662
+ const lift = cue.lift > 0 ? cue.lift * Math.sin(Math.PI * u) : 0;
2663
+ const last = i === n - 1;
2664
+ const h = Math.min(CHOREO_HANDOVER, cue.beats / 2, next.beats / 2);
2665
+ const ahead = smooth3((beat - (cue.beats - h)) / h);
2666
+ if (ahead > 0) {
2667
+ if (next.face === "cam") faceCam += (1 - faceCam) * ahead;
2668
+ else if (next.face !== void 0 && !last) {
2669
+ face += angleTo(face, next.face) * ahead;
2670
+ faceCam *= 1 - ahead;
2671
+ } else if (last) {
2672
+ face += angleTo(face, next.face === void 0 ? 0 : next.face) * ahead;
2673
+ faceCam *= 1 - ahead;
2674
+ }
2675
+ }
2676
+ const lean = (cue.lean ?? 0) + ((next.lean ?? 0) - (cue.lean ?? 0)) * ahead;
2677
+ let squash = (cue.squash ?? 1) + ((next.squash ?? 1) - (cue.squash ?? 1)) * ahead;
2678
+ if (next.lift > 0) squash *= 1 - 0.16 * ahead;
2679
+ if (cue.lift > 0) squash *= u < 0.15 ? 0.84 + 0.26 * smooth3(u / 0.15) : 1 + 0.1 * Math.sin(Math.PI * u) - 0.16 * smooth3((u - 0.85) / 0.15);
2680
+ if (prev.lift > 0 && n > 1) squash *= 0.84 + 0.16 * smooth3(beat / Math.min(0.75, cue.beats));
2681
+ const groove = cue.groove ?? 0;
2682
+ if (groove > 0 && cue.lift === 0) squash *= 1 - groove * (1 + Math.cos(2 * Math.PI * posMod(beats, 1))) / 2;
2683
+ return { cue, beat, u, next, ahead, side, depth, centre, face, faceCam, turn, lift, lean, squash };
2684
+ }
2685
+ function angleTo(a, b) {
2686
+ return posMod(b - a + 180, 360) - 180;
2687
+ }
2688
+
2503
2689
  // src/vignette.ts
2504
2690
  var DANCES = ["toprock", "footwork", "windmill", "headspin", "freeze", "drop"];
2505
- var GESTURES = ["talk", "nod", "shake", "hop", "spin", "wiggle", "rest", "peek", "bow", "ink", ...DANCES];
2506
- var BREED_GESTURES = { ink: ["octopus"] };
2691
+ var GESTURES = ["talk", "nod", "shake", "hop", "spin", "wiggle", "rest", "peek", "bow", "ink", "puff", ...DANCES];
2692
+ var BREED_GESTURES = { ink: ["octopus"], puff: ["blowfish"] };
2507
2693
  var OPEN_MARKS = {
2508
2694
  centre: { x: 0, y: 36, z: 0 },
2509
2695
  left: { x: -70, y: 34, z: 0 },
@@ -2788,6 +2974,7 @@ function poseOf(v, actor, tSec, own) {
2788
2974
  pitch = -0.08 * e;
2789
2975
  break;
2790
2976
  // ink: the body holds still; the octopus squirts its cloud (the tank's InkLayer) and pales.
2977
+ // puff: the body holds still; the blowfish gulps itself round (puffer.ts PufferInput.puffCue).
2791
2978
  // The dances, for a fish with no clips for them: the whole body does what it can.
2792
2979
  case "toprock":
2793
2980
  roll += Math.sin(bt * Math.PI * 2) * 0.3 * e;
@@ -3121,23 +3308,25 @@ var PARAM_DOCS = {
3121
3308
  cameraAzimuth: "Orbit bearing in degrees. 0 looks at the village from the front.",
3122
3309
  cameraElevation: "Degrees above the floor. 8\u201315 is eye level with the fish; 40+ looks down on a castle.",
3123
3310
  cameraDistance: "Distance from the tank centre. 80 is a close-up, 170\u2013220 a village, 300+ a landmark.",
3124
- shot: "A framing: `orbit` (default), `hero` (three-quarter), `front` (level, long lens), `low` (looking up), `top` (a map), `surface` (the underside), `macro` (nearest landmark), `cast` (the mix's first three). Azimuth and autoRotate turn it, distance scales it.",
3311
+ shot: "A framing: `orbit`, `hero` (three-quarter), `front` (level, long lens), `low`, `top` (a map), `surface`, `macro` (nearest landmark), `cast` (the mix's first three). Azimuth and autoRotate turn it, distance scales it.",
3125
3312
  shotBlend: "Seconds a change of `shot` takes (0 = a cut): the camera moves to the new framing, eased.",
3126
- cameraFollow: "Ride with one fish (cast slot); -1 is the orbit camera, whose params are ignored while following. With `followSpot` on the slot, a spotlit chase.",
3127
- followAngle: "Where round the followed fish the chase camera stands, degrees: 0 behind (default), 90 beside, 180 in front looking back at its face.",
3128
- followDistance: "How far behind the followed fish the camera rides (40\u201370 is a chase). Under ~18 it is the fish's eye; a floor walker sees from its own head height.",
3129
- autoRotate: "Turntable speed, degrees/second. 0 holds the shot (stages, vignettes); 1\u20132 is a slow reveal. frame.state.camera.bearing: where it stands now.",
3130
- followAvoid: "Keep the followed fish in view, 0\u20131: the chase camera rises over low clutter or pulls in, eased. For walks through a village.",
3131
- nearFade: "A creature at the lens dissolves instead of being sliced, 0\u20131. The followed fish never fades.",
3313
+ cameraFollow: "Ride with one fish (cast slot); -1 is the orbit camera (its params ignored while following).",
3314
+ followAngle: "Where round the followed fish the chase camera stands, degrees: 0 behind (default), 90 beside, 180 facing it. A choreographed dancer is measured from the stage's facing instead.",
3315
+ followDistance: "How far behind the followed fish the camera rides (40\u201370 is a chase). Under ~18 it is the fish's eye (a walker's: its head height).",
3316
+ autoRotate: "Turntable speed, degrees/second. 0 holds the shot, 1\u20132 a slow reveal. frame.state.camera.bearing: where it stands now.",
3317
+ followAvoid: "Keep the followed fish in view, 0\u20131: the chase camera rises or pulls in, eased.",
3318
+ followHeight: "Chase camera height in body lengths over its feet, aimed at them (0.2: feet); -1 off.",
3319
+ followLens: "Chase lens, degrees (0: 55; 25 long).",
3320
+ nearFade: "A creature at the lens dissolves, not sliced, 0\u20131. The followed fish never fades.",
3132
3321
  // cast
3133
3322
  fishCount: "How many fish when `fishMix` is empty (all are `fishUrl`).",
3134
3323
  finish: "A final full-screen pass: gentle grade, soft vignette, dither against banding, and restrained bloom on high-end devices (0 = none; never on the low tier).",
3135
3324
  tiltShift: "Tilt-shift, 0\u20131: sharp through the middle, soft above and below, like a model. Mid/high tiers.",
3136
- depthOfField: "Depth of field, 0\u20131: the subject sharp (followed fish, `cast` leads), the rest soft. High tier.",
3325
+ depthOfField: "Depth of field, 0\u20131: the subject (followed fish, `cast` leads) sharp, the rest soft. High tier.",
3137
3326
  ambientOcclusion: "Ambient occlusion, 0\u20131: crevices darken. High tier.",
3138
- highlights: "Bright light rolls off instead of clipping, 0\u20131: a fish in a shaft stays a fish. Bright rooms want it. Mid/high tiers.",
3327
+ highlights: "Bright light rolls off instead of clipping, 0\u20131: a fish in a shaft stays a fish. Mid/high tiers.",
3139
3328
  fishUrl: "The single model used when `fishMix` is empty: ipfs://, https://assets.idlescreens.com/, or a URL on the page's own origin. Any other host is refused and the default fish swims.",
3140
- fishMix: "The cast: `id[:count][@style]`, comma-separated: `100:2@drift, 257:1, seahorse:3, shark:1@patrol`. Minted breeds plus bundled: shark crab dori glowfish babyfish hackerfish blowfish starfish octopus snail dot dolphin. Floor walkers keep their own pace: crab, starfish (dances with `starfishDance`), snail. `dot` (a courier box on legs) and `dolphin` (chatters, flips, breaches; stands on its tail to act) play vignette gestures. Overrides fishCount/fishUrl.",
3329
+ fishMix: "The cast: `id[:count][@style]`, comma-separated: `100:2@drift, seahorse:3, shark:1@patrol`. Minted breeds plus bundled: shark crab dori glowfish babyfish hackerfish blowfish starfish octopus snail dot dolphin. Floor walkers keep their own pace: crab, starfish (dances: `starfishDance`, `choreography`), snail. `dot` (a box on legs), `dolphin` (flips, breaches) play vignette gestures. Overrides fishCount/fishUrl.",
3141
3330
  dracoPath: "Where the Draco decoder lives; leave empty on a channel. Only the page's own origin or the shipped copy is used.",
3142
3331
  swimSpeed: "Global swim speed multiplier. 0.4 is contemplative, 1 lively.",
3143
3332
  swimStyle: "How the cast swims unless a fishMix entry says otherwise (`@style`). `auto` picks by breed.",
@@ -3161,14 +3350,16 @@ var PARAM_DOCS = {
3161
3350
  fishGlow: "How strongly GLOW- parts of a fish bloom and light their neighbours and the floor.",
3162
3351
  fishMetal: "`on` (default) gives METAL- parts a polished, reflective finish.",
3163
3352
  snailShell: "Snail shells: `banded`, `pearl` (iridescent), `gold`, `glow` (a spiral breathing light; best at night); `mix` (default), each its own. Lit fish only. frame.state.fish[i].shell says which.",
3164
- starfishDance: "Starfish dance: `aerobics`, a class in unison facing the camera; `freestyle`, each its own move every eight counts; `duet`, couples face to face (with `spotRig: 0/#ffd27a*26, 1/#ff8ad0*26`); `off` (default). Cast a few (`starfish:7`), `shot: front`, `autoRotate: 0`, a bright room.",
3165
- danceTempo: "Beats per minute for the starfish dance (60\u2013180, default 128). Glides on the beat.",
3353
+ starfishDance: "Starfish dance: `aerobics` (a class in unison), `freestyle` (a move each per eight counts), `duet` (couples), `off`. Cast a few: `starfish:7`.",
3354
+ danceTempo: "Dance BPM; glides on the beat.",
3355
+ choreography: "Starfish dance cues, on the beat (see grammar).",
3166
3356
  fishLook: "`neon` dresses the bundled creatures for blacklight: near-black coats, eyes (and a crab's mouth) glowing a neon of their own. Best in a dark room with `finish`. Default `natural`.",
3167
3357
  fishRim: "A thin edge of the water's light round each fish, 0\u20131 (lit mode): dark fish on dark water. 0.6\u20131 in dark rooms.",
3168
- fishShadows: "Soft shadows on the ground under the swimmers, 0\u20131: dark near the sand, faint high up.",
3169
- fishFadeIn: "Seconds an arriving fish takes to dissolve in (0 = it pops in).",
3170
- sceneryShadows: "Soft shadows under rocks, homes, castle, pyramids, geodes and lamps, 0\u20131, so nothing floats.",
3171
- voxelAO: "Plant cubes darken where they touch each other and the sand, 0\u20131: solid coral. All tiers.",
3358
+ fishShadows: "Soft shadows under the swimmers, 0\u20131: dark near the sand, faint high up.",
3359
+ fishAO: "A fish's own corners darken, 0\u20131: solid fish. All tiers.",
3360
+ fishFadeIn: "Seconds an arriving fish takes to fade in (0 pops in).",
3361
+ sceneryShadows: "Soft shadows under rocks, homes, castle, pyramids, geodes, lamps, 0\u20131: nothing floats.",
3362
+ voxelAO: "Plant cubes darken where they touch each other and the sand, 0\u20131. All tiers.",
3172
3363
  swimWave: "Fish bend as they swim: a wave nose to tail, the body curling into turns (0 = rigid wiggle). Skips the turtle, crab and fish with a skeleton; the seahorse gets fin, tail and nod instead.",
3173
3364
  fishAvoid: "Fish make way for each other: a pair about to meet parts a second ahead, the smaller giving more, over or under before round. 1 (default) on, 0 pass-through. `inspect().crowding` counts pairs still touching.",
3174
3365
  eyeLife: "Eyes blink, look, and emote (0 = painted on). Redraws each token's own pixel-grid eye; black and white only.",
@@ -3188,7 +3379,7 @@ var PARAM_DOCS = {
3188
3379
  waterY: "Height of the water ceiling; -1 = the environment's own.",
3189
3380
  rayStrength: "Surface light shafts, within device budget: 5 high tier, 2 standard; none on the lowest tier with a surface (abyss, vent, universe keep 2; perceiveChannel then reads 0).",
3190
3381
  raySoftness: "Shafts as soft volume, not flat sheets, 0\u20131. 1 for anything cinematic.",
3191
- floorDetail: "A seabed, not a coloured plane, 0\u20131: ripples, silt, grain; the floor's far edge fades into the fog.",
3382
+ floorDetail: "A seabed, not a coloured plane, 0\u20131: ripples, silt, grain; its far edge fades into the fog.",
3192
3383
  caustics: "The dancing net of light from the surface on the floor, rocks, plants and fish (0 = none). Sharp near the top, soft and dim deep down.",
3193
3384
  causticScale: "Size of the caustic cells: below 1 a fine shimmer, above 1 broad slow bands.",
3194
3385
  surfaceMirror: "The surface from below: a window to the light in a rippling mirror of the tank (0 = plain). Needs reef, kelp, ice or lagoon; for the `surface` and `low` shots. High tier mirrors the tank, mid the deep water.",
@@ -3243,7 +3434,7 @@ var PARAM_DOCS = {
3243
3434
  spotColor: "Colour of the single `followSpot`.",
3244
3435
  spotShadow: "The spotted fish's shadow in its pool. 0 none, 1 true size.",
3245
3436
  spotRig: "Up to three spots: `slot[/color][*radius]` follows a fish, `@mark` stands on a mark (`@mic` lights whoever steps up). They are a, b, c in order. Overrides followSpot.",
3246
- spotCues: "A looping cue sheet for the rig: `8s:a, 8s:b, 12s:a+b, 4s:-` (`-` = blackout). Spots cross-fade at each cue.",
3437
+ spotCues: "Looping cues for the rig: `8s:a, 12s:a+b, 4s:-` (`-` blackout, cues \u2265 2 s). Spots cross-fade.",
3247
3438
  vignette: "A small scene for the first 2\u20133 fish: a preset (tea\xB7bedtime\xB7seek indoors; duet\xB7trio\xB7openmic on the open stage) or a script of beats. A floor creature cast as an actor stays on the floor. Grammar below."
3248
3439
  };
3249
3440
  var GRAMMAR = `
@@ -3269,13 +3460,14 @@ geodeMineral world | mineral, \u2026 amethyst agate celestine citrine carneli
3269
3460
  floraPalette world | #rrggbb, \u2026 (\u22646) world \xB7 #ff4fa0, #ffb347, #4fd1ff
3270
3461
  spotRig slot|@mark[/color][*radius], \u2026 (\u22643; they become a, b, c) 0/#ffd27a*24, 1/#ff8ad0*24, 2/#7fdcff*24
3271
3462
  spotCues <sec>s:<spots>, \u2026 loops 4s:-, 7s:a, 7s:b, 12s:a+b
3463
+ choreography [a|*:] <beats>:<move> [>to @face ~turn ^leap /lean %crouch *groove xspeed =pose],\u2026|\u2026 moves ${STARFISH_MOVE_NAMES.join(" ")}
3272
3464
  signs kind[@place][>target][/#color][*size][:text], \u2026 plank@gate:"Welcome to the reef", neon@home1/#ff4fa0:OPEN, led@plaza:TIDE 4.2M
3273
3465
  kinds plank arrow ring porthole neon led \xB7 places gate plaza courtyard hub fountain home1 home2 home3 centre left right front back
3274
3466
  an arrow points at >target \xB7 text \u2264 plank 24, arrow 16, ring 10, porthole 4 (empty = a fish), neon 14, led 64 (scrolls) \xB7 \u2264 8 signs
3275
3467
  vignette beats separated by |, cues by comma; actors a b c d are fish slots 0 1 2 3
3276
3468
  [<sec>s:] actor =mark be there, no travel (a later beat's = snaps at its start) actor >mark swim there
3277
3469
  actor @mark | @actor face it a b circle mark each named actor rings it, back to its spot by the beat's end; others stay put
3278
- actor follow actor gestures: talk nod shake hop spin wiggle rest peek bow \xB7 ink (octopus only: a cloud of ink)
3470
+ actor follow actor gestures: talk nod shake hop spin wiggle rest peek bow \xB7 ink (octopus) \xB7 puff (blowfish)
3279
3471
  dances: toprock footwork windmill headspin freeze drop (a betafish dances them with its own body; 2 s beats keep them on a 120 bpm bar)
3280
3472
  Give every beat a duration and end where you began and a spotCues sheet stays in step forever.
3281
3473
  Fish cast @audience sit out front facing the stage: they laugh as each talk ends and applaud each bow.
@@ -3288,7 +3480,7 @@ vignette beats separated by |, cues by comma; actors a b c d are fish slots 0 1
3288
3480
  var NIGHT = { fogColor: "#0a0a1a", fogNear: 120, fogFar: 700 };
3289
3481
  var STAGE = { fogColor: "#01030a", floorColor: "#0a1322", fogNear: 120, fogFar: 650, autoRotate: 0 };
3290
3482
  var ALIVE = { eyeLife: 1 };
3291
- var CINEMATIC = { highlights: 1, raySoftness: 1, nearFade: 1, finish: 0.6 };
3483
+ var CINEMATIC = { highlights: 1, raySoftness: 1, nearFade: 1, voxelAO: 1, fishAO: 1, finish: 0.6 };
3292
3484
  var RECIPES = [
3293
3485
  {
3294
3486
  id: "geode-harbor",
@@ -3567,7 +3759,7 @@ var RECIPES = [
3567
3759
  {
3568
3760
  id: "starfish-class",
3569
3761
  label: "Starfish aerobics",
3570
- what: "Seven starfish stand up on a pale ice floor, face the camera and dance an aerobics routine in unison, an instructor in front.",
3762
+ what: "Seven starfish stand up on a pale ice floor, face the camera and dance aerobics in unison, an instructor in front.",
3571
3763
  params: { environment: "ice", fishMix: "starfish:7", starfishDance: "aerobics", danceTempo: 120, shot: "front", autoRotate: 0 }
3572
3764
  },
3573
3765
  {
@@ -3609,7 +3801,7 @@ var RECIPES = [
3609
3801
  {
3610
3802
  id: "snail-night",
3611
3803
  label: "Snails by night",
3612
- what: "Glowing snails on a dark floor: each spiral breathes light, the feelers pool green on the sand, and the camera rides beside one.",
3804
+ what: "Glowing snails on a dark floor: each spiral breathes light, feelers pool green on the sand, the camera beside one.",
3613
3805
  params: {
3614
3806
  ...ALIVE,
3615
3807
  ...NIGHT,
@@ -3628,7 +3820,7 @@ var RECIPES = [
3628
3820
  {
3629
3821
  id: "reef-cinematic",
3630
3822
  label: "Reef, cinematic",
3631
- what: "A small reef cast framed close wherever it swims, in soft shafts, each fish over its shadow. The polished look.",
3823
+ what: "A small reef cast framed close wherever it swims, in soft shafts, each fish over its shadow.",
3632
3824
  params: {
3633
3825
  ...ALIVE,
3634
3826
  ...CINEMATIC,
@@ -3653,7 +3845,7 @@ var RECIPES = [
3653
3845
  {
3654
3846
  id: "night-cinematic",
3655
3847
  label: "Deep night, cinematic",
3656
- what: "The deep at night: glowfish under jellyfish lanterns, a shark sliding through, every outline rimmed in light.",
3848
+ what: "The deep at night: glowfish under jellyfish lanterns, a shark sliding through, outlines rimmed in light.",
3657
3849
  params: {
3658
3850
  ...ALIVE,
3659
3851
  ...CINEMATIC,
@@ -3677,7 +3869,7 @@ var RECIPES = [
3677
3869
  {
3678
3870
  id: "village-cinematic",
3679
3871
  label: "Village, cinematic",
3680
- what: "Dot, Puff and Hex meet in a sunlit village of stone homes, seen high through a tilt-shift lens: a model village.",
3872
+ what: "Dot, Puff and Hex meet in a sunlit village of stone homes, seen through a tilt-shift lens.",
3681
3873
  params: {
3682
3874
  ...ALIVE,
3683
3875
  ...CINEMATIC,
@@ -3725,6 +3917,7 @@ function validateMetaquariumParams(params) {
3725
3917
  if (str("townProps")) push("townProps", parseTownProps(str("townProps")).problems);
3726
3918
  if (str("floorCover")) push("floorCover", parseFloorCover(str("floorCover")).problems);
3727
3919
  if (str("pyramidShow")) push("pyramidShow", parsePyramidShow(str("pyramidShow")).problems);
3920
+ if (str("choreography")) push("choreography", parseChoreography(str("choreography"), STARFISH_MOVE_NAMES).problems);
3728
3921
  if (str("geodeMineral")) push("geodeMineral", parseGeodeMineral(str("geodeMineral")).problems);
3729
3922
  const rig = parseSpotRig(str("spotRig"));
3730
3923
  push("spotRig", rig.problems);
@@ -3841,9 +4034,11 @@ function dependencyProblems(params) {
3841
4034
  if (dance !== "off" && !starfishSwims) {
3842
4035
  out.push({ path: "starfishDance", also: ["fishMix"], message: "no starfish in the cast to dance \u2014 add e.g. starfish:7 (slots past the cap never swim)" });
3843
4036
  }
3844
- if (set("danceTempo") && dance === "off") out.push({ path: "danceTempo", also: ["starfishDance"], message: "does nothing while starfishDance is off" });
4037
+ const choreographed = !!str("choreography").trim();
4038
+ if (set("danceTempo") && dance === "off" && !choreographed) out.push({ path: "danceTempo", also: ["starfishDance"], message: "does nothing while starfishDance is off" });
4039
+ if (choreographed && !starfishSwims) out.push({ path: "choreography", also: ["fishMix"], message: "no starfish in the cast to dance it \u2014 add e.g. starfish:5" });
3845
4040
  if (follow < 0) {
3846
- for (const k of ["followDistance", "followAngle", "followAvoid"]) if (set(k)) out.push({ path: k, also: ["cameraFollow"], message: "does nothing while cameraFollow is -1 (the orbit camera)" });
4041
+ for (const k of ["followDistance", "followAngle", "followAvoid", "followHeight", "followLens"]) if (set(k)) out.push({ path: k, also: ["cameraFollow"], message: "does nothing while cameraFollow is -1 (the orbit camera)" });
3847
4042
  } else if (follow < cast) {
3848
4043
  for (const k of ORBIT_PARAMS) if (set(k)) out.push({ path: k, also: ["cameraFollow"], message: `ignored while cameraFollow rides with slot ${follow} \u2014 they return when it is -1` });
3849
4044
  }
@@ -3995,6 +4190,7 @@ function describeMetaquarium(params, timed = []) {
3995
4190
  stage.push(VIGNETTES[v] ? `vignette: ${v}` : `vignette: ${v.split(/[|\n]/).filter((b) => b.trim()).length} beats`);
3996
4191
  }
3997
4192
  if (str("starfishDance") !== "off") stage.push(`starfish ${str("starfishDance")}${has("danceTempo") ? ` at ${num("danceTempo")} bpm` : ""}`);
4193
+ if (str("choreography").trim()) stage.push(`a starfish choreography${has("danceTempo") ? ` at ${num("danceTempo")} bpm` : ""}`);
3998
4194
  const camera = [];
3999
4195
  const follow = Math.round(num("cameraFollow"));
4000
4196
  if (follow >= 0) {
@@ -4074,7 +4270,8 @@ var METAQUARIUM_PARAMS = {
4074
4270
  followDistance: { type: "number", default: 45, min: 0, max: 160, ease: "smooth" },
4075
4271
  /** Where round the followed fish the chase camera stands, degrees: 0 behind
4076
4272
  * it (the default), 90 at its side, 180 in front looking back at its face
4077
- * — a hackerfish's screen, a crab's smile. */
4273
+ * — a hackerfish's screen, a crab's smile. A choreographed dancer is framed
4274
+ * from its stage's front instead, so a spin turns the dancer, not the shot. */
4078
4275
  followAngle: { type: "number", default: 0, min: -180, max: 180, ease: "smooth" },
4079
4276
  /** Keep the followed fish in view (sightline.ts): when a home, a wall, a lamp
4080
4277
  * or a kelp stand comes between it and the chase camera, the camera rises a
@@ -4082,6 +4279,15 @@ var METAQUARIUM_PARAMS = {
4082
4279
  * a game camera's spring arm does — eased, never a snap. 0 (default) is the
4083
4280
  * plain chase camera; 1 the full arm. Not the fish's-eye view (it has none). */
4084
4281
  followAvoid: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
4282
+ /** The chase camera's height over the followed fish's feet, in its body
4283
+ * lengths. -1 (default) rides a little above it, looking at its middle;
4284
+ * set, the camera stands at that height aimed at its feet — 0.1–0.3 on a
4285
+ * long lens is a shot of a walker's feet, the floor behind them. */
4286
+ followHeight: { type: "number", default: -1, min: -1, max: 3, ease: "step" },
4287
+ /** The chase camera's lens: degrees of view, top to bottom. 0 (default) is
4288
+ * the orbit camera's 55; 20–30 is a long lens — flatter and tighter, from
4289
+ * further back. */
4290
+ followLens: { type: "number", default: 0, min: 0, max: 90, ease: "step" },
4085
4291
  /** Continuous orbit speed, degrees/second. Zero by default: the tank is
4086
4292
  * still, letting the fish movement carry the scene. Steer up for an orbit. */
4087
4293
  autoRotate: { type: "number", default: 0, min: 0, max: 12, ease: "smooth" },
@@ -4284,6 +4490,10 @@ var METAQUARIUM_PARAMS = {
4284
4490
  * mode), brighter on top: what separates a dark fish from dark water.
4285
4491
  * 0 is none. */
4286
4492
  fishRim: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
4493
+ /** The fish's own corners darken — a fin's root, the step at the head, the
4494
+ * gap under a shell — read off the voxels they are built from. Baked once
4495
+ * per breed, the first frame it is on; every tier. 0 is off. */
4496
+ fishAO: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
4287
4497
  /** A soft shadow on the ground under every swimmer: small and dark near
4288
4498
  * the sand, wide and faint high up — what puts a fish at a height instead
4289
4499
  * of floating in front of the floor. 0 is none (floor walkers always
@@ -4352,6 +4562,13 @@ var METAQUARIUM_PARAMS = {
4352
4562
  /** The dance's tempo, beats per minute. Every move is a bar of four beats.
4353
4563
  * Integrated when steered, so a glide speeds the class up on the beat. */
4354
4564
  danceTempo: { type: "number", default: 128, min: 60, max: 180, ease: "smooth" },
4565
+ /** An authored dance on the beat (choreo.ts): a cue sheet per dancer —
4566
+ * `a: 8:lie, 4:rise, 4:reach >right @cam | *: 4:march ~360, 4:jacks ^0.5`.
4567
+ * Set, every starfish dances it (in the class's places, or the couples'
4568
+ * under `starfishDance: duet`), facing `cameraAzimuth`; a dancer it leaves
4569
+ * out keeps `starfishDance`'s routine, or stands and waits. Beats on the
4570
+ * `danceTempo` clock from the start, so a sheet lines up with music. */
4571
+ choreography: { type: "string", default: "", ease: "step" },
4355
4572
  /** Eye life, 0..1: blinks on a personal clock, idle saccades, pupils that
4356
4573
  * lead a turn or a climb, eyes on whoever a vignette has it talking to, a
4357
4574
  * glance at the camera now and then, wide for a hop and shut for a rest.
@@ -4751,6 +4968,10 @@ export {
4751
4968
  SIGN_TEXT_MAX,
4752
4969
  MAX_SIGNS,
4753
4970
  parseSignMix,
4971
+ STARFISH_MOVE_NAMES,
4972
+ parseChoreography,
4973
+ choreoLine,
4974
+ choreoAt,
4754
4975
  GESTURES,
4755
4976
  OPEN_MARKS,
4756
4977
  parseVignette,
@@ -4777,4 +4998,4 @@ export {
4777
4998
  paramSpaceWith,
4778
4999
  coerceNum
4779
5000
  };
4780
- //# sourceMappingURL=chunk-5DJ5WBVN.js.map
5001
+ //# sourceMappingURL=chunk-M2PKNQNY.js.map