@idle-screens/saver-metaquarium 0.21.0 → 0.22.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,190 @@ 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 = /^([>@~^/%*])(.+)$/.exec(tok);
2568
+ if (!mod) {
2569
+ problems.push(`"${raw}": "${tok}" is not >to, @face, ~turn, ^leap, /lean, %crouch or *groove`);
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 (Number.isFinite(Number(arg)) && Number(arg) > 0) cue.groove = clamp(Number(arg), 0.02, 0.3);
2592
+ else problems.push(`"${raw}": groove "${arg}" is not a depth`);
2593
+ } else if (op === "~") {
2594
+ if (Number.isFinite(Number(arg))) cue.turn = clamp(Number(arg), -1440, 1440);
2595
+ else problems.push(`"${raw}": turn "${arg}" is not degrees`);
2596
+ } else {
2597
+ if (Number.isFinite(Number(arg)) && Number(arg) >= 0) cue.lift = Math.min(Number(arg), 3);
2598
+ else problems.push(`"${raw}": leap "${arg}" is not a height`);
2599
+ }
2600
+ }
2601
+ cues.push(cue);
2602
+ b0 += cue.beats;
2603
+ }
2604
+ if (cues.length) lines.set(actor, { cues, beats: b0 });
2605
+ else problems.push(`line "${rawLine}" has no cues`);
2606
+ }
2607
+ return { lines, problems };
2608
+ }
2609
+ var CHOREO_HANDOVER = 0.5;
2610
+ var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
2611
+ var smooth3 = (u) => {
2612
+ const c = clamp(u, 0, 1);
2613
+ return c * c * (3 - 2 * c);
2614
+ };
2615
+ var posMod = (v, m) => (v % m + m) % m;
2616
+ function choreoLine(sheet, slot) {
2617
+ return sheet?.lines.get(slot) ?? sheet?.lines.get("*") ?? null;
2618
+ }
2619
+ function choreoAt(line, beats) {
2620
+ const b = posMod(beats, line.beats);
2621
+ const n = line.cues.length;
2622
+ let i = 0;
2623
+ while (i < n - 1 && b >= line.cues[i].b0 + line.cues[i].beats) i++;
2624
+ const cue = line.cues[i], next = line.cues[(i + 1) % n], prev = line.cues[(i + n - 1) % n];
2625
+ let side = 0, depth = 0, centre = 0, face = 0, faceCam = 0, turn = 0;
2626
+ const end = (c) => {
2627
+ if (c.to === "centre") centre = 1;
2628
+ else if (c.to) {
2629
+ side = c.to[0];
2630
+ depth = c.to[1];
2631
+ centre = 0;
2632
+ }
2633
+ turn += c.turn;
2634
+ };
2635
+ const facing = (c) => {
2636
+ if (c.face === "cam") faceCam = 1;
2637
+ else if (c.face !== void 0) {
2638
+ face = c.face;
2639
+ faceCam = 0;
2640
+ }
2641
+ };
2642
+ for (let k = 0; k < i; k++) {
2643
+ end(line.cues[k]);
2644
+ facing(line.cues[k]);
2645
+ }
2646
+ facing(cue);
2647
+ const beat = b - cue.b0, u = clamp(beat / cue.beats, 0, 1);
2648
+ const s = smooth3(u);
2649
+ if (cue.to === "centre") centre += (1 - centre) * s;
2650
+ else if (cue.to) {
2651
+ side += (cue.to[0] - side) * s;
2652
+ depth += (cue.to[1] - depth) * s;
2653
+ centre *= 1 - s;
2654
+ }
2655
+ turn += cue.turn * s;
2656
+ const lift = cue.lift > 0 ? cue.lift * Math.sin(Math.PI * u) : 0;
2657
+ const last = i === n - 1;
2658
+ const h = Math.min(CHOREO_HANDOVER, cue.beats / 2, next.beats / 2);
2659
+ const ahead = smooth3((beat - (cue.beats - h)) / h);
2660
+ if (ahead > 0) {
2661
+ if (next.face === "cam") faceCam += (1 - faceCam) * ahead;
2662
+ else if (next.face !== void 0 && !last) {
2663
+ face += angleTo(face, next.face) * ahead;
2664
+ faceCam *= 1 - ahead;
2665
+ } else if (last) {
2666
+ face += angleTo(face, next.face === void 0 ? 0 : next.face) * ahead;
2667
+ faceCam *= 1 - ahead;
2668
+ }
2669
+ }
2670
+ const lean = (cue.lean ?? 0) + ((next.lean ?? 0) - (cue.lean ?? 0)) * ahead;
2671
+ let squash = (cue.squash ?? 1) + ((next.squash ?? 1) - (cue.squash ?? 1)) * ahead;
2672
+ if (next.lift > 0) squash *= 1 - 0.16 * ahead;
2673
+ 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);
2674
+ if (prev.lift > 0 && n > 1) squash *= 0.84 + 0.16 * smooth3(beat / Math.min(0.75, cue.beats));
2675
+ const groove = cue.groove ?? 0;
2676
+ if (groove > 0 && cue.lift === 0) squash *= 1 - groove * (1 + Math.cos(2 * Math.PI * posMod(beats, 1))) / 2;
2677
+ return { cue, beat, u, next, ahead, side, depth, centre, face, faceCam, turn, lift, lean, squash };
2678
+ }
2679
+ function angleTo(a, b) {
2680
+ return posMod(b - a + 180, 360) - 180;
2681
+ }
2682
+
2503
2683
  // src/vignette.ts
2504
2684
  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"] };
2685
+ var GESTURES = ["talk", "nod", "shake", "hop", "spin", "wiggle", "rest", "peek", "bow", "ink", "puff", ...DANCES];
2686
+ var BREED_GESTURES = { ink: ["octopus"], puff: ["blowfish"] };
2507
2687
  var OPEN_MARKS = {
2508
2688
  centre: { x: 0, y: 36, z: 0 },
2509
2689
  left: { x: -70, y: 34, z: 0 },
@@ -2788,6 +2968,7 @@ function poseOf(v, actor, tSec, own) {
2788
2968
  pitch = -0.08 * e;
2789
2969
  break;
2790
2970
  // ink: the body holds still; the octopus squirts its cloud (the tank's InkLayer) and pales.
2971
+ // puff: the body holds still; the blowfish gulps itself round (puffer.ts PufferInput.puffCue).
2791
2972
  // The dances, for a fish with no clips for them: the whole body does what it can.
2792
2973
  case "toprock":
2793
2974
  roll += Math.sin(bt * Math.PI * 2) * 0.3 * e;
@@ -3121,23 +3302,23 @@ var PARAM_DOCS = {
3121
3302
  cameraAzimuth: "Orbit bearing in degrees. 0 looks at the village from the front.",
3122
3303
  cameraElevation: "Degrees above the floor. 8\u201315 is eye level with the fish; 40+ looks down on a castle.",
3123
3304
  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.",
3305
+ 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
3306
  shotBlend: "Seconds a change of `shot` takes (0 = a cut): the camera moves to the new framing, eased.",
3126
3307
  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
3308
  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
3309
  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
3310
  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
3311
  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.",
3312
+ nearFade: "A creature at the lens dissolves, not sliced, 0\u20131. The followed fish never fades.",
3132
3313
  // cast
3133
3314
  fishCount: "How many fish when `fishMix` is empty (all are `fishUrl`).",
3134
3315
  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
3316
  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.",
3317
+ depthOfField: "Depth of field, 0\u20131: the subject (followed fish, `cast` leads) sharp, the rest soft. High tier.",
3137
3318
  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.",
3319
+ highlights: "Bright light rolls off instead of clipping, 0\u20131: a fish in a shaft stays a fish. Mid/high tiers.",
3139
3320
  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.",
3321
+ 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
3322
  dracoPath: "Where the Draco decoder lives; leave empty on a channel. Only the page's own origin or the shipped copy is used.",
3142
3323
  swimSpeed: "Global swim speed multiplier. 0.4 is contemplative, 1 lively.",
3143
3324
  swimStyle: "How the cast swims unless a fishMix entry says otherwise (`@style`). `auto` picks by breed.",
@@ -3161,14 +3342,16 @@ var PARAM_DOCS = {
3161
3342
  fishGlow: "How strongly GLOW- parts of a fish bloom and light their neighbours and the floor.",
3162
3343
  fishMetal: "`on` (default) gives METAL- parts a polished, reflective finish.",
3163
3344
  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.",
3345
+ starfishDance: "Starfish dance: `aerobics` (a class in unison), `freestyle` (a move each per eight counts), `duet` (couples), `off`. Cast a few: `starfish:7`.",
3346
+ danceTempo: "Dance BPM; glides on the beat.",
3347
+ choreography: "Starfish dance cues, on the beat (see grammar).",
3166
3348
  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
3349
  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.",
3350
+ fishShadows: "Soft shadows under the swimmers, 0\u20131: dark near the sand, faint high up.",
3351
+ fishAO: "A fish's own corners darken, 0\u20131: solid fish. All tiers.",
3352
+ fishFadeIn: "Seconds an arriving fish takes to fade in (0 pops in).",
3353
+ sceneryShadows: "Soft shadows under rocks, homes, castle, pyramids, geodes, lamps, 0\u20131: nothing floats.",
3354
+ voxelAO: "Plant cubes darken where they touch each other and the sand, 0\u20131. All tiers.",
3172
3355
  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
3356
  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
3357
  eyeLife: "Eyes blink, look, and emote (0 = painted on). Redraws each token's own pixel-grid eye; black and white only.",
@@ -3188,7 +3371,7 @@ var PARAM_DOCS = {
3188
3371
  waterY: "Height of the water ceiling; -1 = the environment's own.",
3189
3372
  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
3373
  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.",
3374
+ floorDetail: "A seabed, not a coloured plane, 0\u20131: ripples, silt, grain; its far edge fades into the fog.",
3192
3375
  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
3376
  causticScale: "Size of the caustic cells: below 1 a fine shimmer, above 1 broad slow bands.",
3194
3377
  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 +3426,7 @@ var PARAM_DOCS = {
3243
3426
  spotColor: "Colour of the single `followSpot`.",
3244
3427
  spotShadow: "The spotted fish's shadow in its pool. 0 none, 1 true size.",
3245
3428
  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.",
3429
+ spotCues: "Looping cues for the rig: `8s:a, 12s:a+b, 4s:-` (`-` blackout, cues \u2265 2 s). Spots cross-fade.",
3247
3430
  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
3431
  };
3249
3432
  var GRAMMAR = `
@@ -3269,13 +3452,14 @@ geodeMineral world | mineral, \u2026 amethyst agate celestine citrine carneli
3269
3452
  floraPalette world | #rrggbb, \u2026 (\u22646) world \xB7 #ff4fa0, #ffb347, #4fd1ff
3270
3453
  spotRig slot|@mark[/color][*radius], \u2026 (\u22643; they become a, b, c) 0/#ffd27a*24, 1/#ff8ad0*24, 2/#7fdcff*24
3271
3454
  spotCues <sec>s:<spots>, \u2026 loops 4s:-, 7s:a, 7s:b, 12s:a+b
3455
+ choreography [a|*:] <beats>:<move> [>to @face ~turn ^leap /lean %crouch *groove],\u2026|\u2026 moves ${STARFISH_MOVE_NAMES.join(" ")}
3272
3456
  signs kind[@place][>target][/#color][*size][:text], \u2026 plank@gate:"Welcome to the reef", neon@home1/#ff4fa0:OPEN, led@plaza:TIDE 4.2M
3273
3457
  kinds plank arrow ring porthole neon led \xB7 places gate plaza courtyard hub fountain home1 home2 home3 centre left right front back
3274
3458
  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
3459
  vignette beats separated by |, cues by comma; actors a b c d are fish slots 0 1 2 3
3276
3460
  [<sec>s:] actor =mark be there, no travel (a later beat's = snaps at its start) actor >mark swim there
3277
3461
  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)
3462
+ actor follow actor gestures: talk nod shake hop spin wiggle rest peek bow \xB7 ink (octopus) \xB7 puff (blowfish)
3279
3463
  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
3464
  Give every beat a duration and end where you began and a spotCues sheet stays in step forever.
3281
3465
  Fish cast @audience sit out front facing the stage: they laugh as each talk ends and applaud each bow.
@@ -3288,7 +3472,7 @@ vignette beats separated by |, cues by comma; actors a b c d are fish slots 0 1
3288
3472
  var NIGHT = { fogColor: "#0a0a1a", fogNear: 120, fogFar: 700 };
3289
3473
  var STAGE = { fogColor: "#01030a", floorColor: "#0a1322", fogNear: 120, fogFar: 650, autoRotate: 0 };
3290
3474
  var ALIVE = { eyeLife: 1 };
3291
- var CINEMATIC = { highlights: 1, raySoftness: 1, nearFade: 1, finish: 0.6 };
3475
+ var CINEMATIC = { highlights: 1, raySoftness: 1, nearFade: 1, voxelAO: 1, fishAO: 1, finish: 0.6 };
3292
3476
  var RECIPES = [
3293
3477
  {
3294
3478
  id: "geode-harbor",
@@ -3567,7 +3751,7 @@ var RECIPES = [
3567
3751
  {
3568
3752
  id: "starfish-class",
3569
3753
  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.",
3754
+ what: "Seven starfish stand up on a pale ice floor, face the camera and dance aerobics in unison, an instructor in front.",
3571
3755
  params: { environment: "ice", fishMix: "starfish:7", starfishDance: "aerobics", danceTempo: 120, shot: "front", autoRotate: 0 }
3572
3756
  },
3573
3757
  {
@@ -3609,7 +3793,7 @@ var RECIPES = [
3609
3793
  {
3610
3794
  id: "snail-night",
3611
3795
  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.",
3796
+ what: "Glowing snails on a dark floor: each spiral breathes light, feelers pool green on the sand, the camera beside one.",
3613
3797
  params: {
3614
3798
  ...ALIVE,
3615
3799
  ...NIGHT,
@@ -3628,7 +3812,7 @@ var RECIPES = [
3628
3812
  {
3629
3813
  id: "reef-cinematic",
3630
3814
  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.",
3815
+ what: "A small reef cast framed close wherever it swims, in soft shafts, each fish over its shadow.",
3632
3816
  params: {
3633
3817
  ...ALIVE,
3634
3818
  ...CINEMATIC,
@@ -3653,7 +3837,7 @@ var RECIPES = [
3653
3837
  {
3654
3838
  id: "night-cinematic",
3655
3839
  label: "Deep night, cinematic",
3656
- what: "The deep at night: glowfish under jellyfish lanterns, a shark sliding through, every outline rimmed in light.",
3840
+ what: "The deep at night: glowfish under jellyfish lanterns, a shark sliding through, outlines rimmed in light.",
3657
3841
  params: {
3658
3842
  ...ALIVE,
3659
3843
  ...CINEMATIC,
@@ -3677,7 +3861,7 @@ var RECIPES = [
3677
3861
  {
3678
3862
  id: "village-cinematic",
3679
3863
  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.",
3864
+ what: "Dot, Puff and Hex meet in a sunlit village of stone homes, seen through a tilt-shift lens.",
3681
3865
  params: {
3682
3866
  ...ALIVE,
3683
3867
  ...CINEMATIC,
@@ -3725,6 +3909,7 @@ function validateMetaquariumParams(params) {
3725
3909
  if (str("townProps")) push("townProps", parseTownProps(str("townProps")).problems);
3726
3910
  if (str("floorCover")) push("floorCover", parseFloorCover(str("floorCover")).problems);
3727
3911
  if (str("pyramidShow")) push("pyramidShow", parsePyramidShow(str("pyramidShow")).problems);
3912
+ if (str("choreography")) push("choreography", parseChoreography(str("choreography"), STARFISH_MOVE_NAMES).problems);
3728
3913
  if (str("geodeMineral")) push("geodeMineral", parseGeodeMineral(str("geodeMineral")).problems);
3729
3914
  const rig = parseSpotRig(str("spotRig"));
3730
3915
  push("spotRig", rig.problems);
@@ -3841,7 +4026,9 @@ function dependencyProblems(params) {
3841
4026
  if (dance !== "off" && !starfishSwims) {
3842
4027
  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
4028
  }
3844
- if (set("danceTempo") && dance === "off") out.push({ path: "danceTempo", also: ["starfishDance"], message: "does nothing while starfishDance is off" });
4029
+ const choreographed = !!str("choreography").trim();
4030
+ if (set("danceTempo") && dance === "off" && !choreographed) out.push({ path: "danceTempo", also: ["starfishDance"], message: "does nothing while starfishDance is off" });
4031
+ 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
4032
  if (follow < 0) {
3846
4033
  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)" });
3847
4034
  } else if (follow < cast) {
@@ -3995,6 +4182,7 @@ function describeMetaquarium(params, timed = []) {
3995
4182
  stage.push(VIGNETTES[v] ? `vignette: ${v}` : `vignette: ${v.split(/[|\n]/).filter((b) => b.trim()).length} beats`);
3996
4183
  }
3997
4184
  if (str("starfishDance") !== "off") stage.push(`starfish ${str("starfishDance")}${has("danceTempo") ? ` at ${num("danceTempo")} bpm` : ""}`);
4185
+ if (str("choreography").trim()) stage.push(`a starfish choreography${has("danceTempo") ? ` at ${num("danceTempo")} bpm` : ""}`);
3998
4186
  const camera = [];
3999
4187
  const follow = Math.round(num("cameraFollow"));
4000
4188
  if (follow >= 0) {
@@ -4284,6 +4472,10 @@ var METAQUARIUM_PARAMS = {
4284
4472
  * mode), brighter on top: what separates a dark fish from dark water.
4285
4473
  * 0 is none. */
4286
4474
  fishRim: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
4475
+ /** The fish's own corners darken — a fin's root, the step at the head, the
4476
+ * gap under a shell — read off the voxels they are built from. Baked once
4477
+ * per breed, the first frame it is on; every tier. 0 is off. */
4478
+ fishAO: { type: "number", default: 0, min: 0, max: 1, ease: "smooth" },
4287
4479
  /** A soft shadow on the ground under every swimmer: small and dark near
4288
4480
  * the sand, wide and faint high up — what puts a fish at a height instead
4289
4481
  * of floating in front of the floor. 0 is none (floor walkers always
@@ -4352,6 +4544,13 @@ var METAQUARIUM_PARAMS = {
4352
4544
  /** The dance's tempo, beats per minute. Every move is a bar of four beats.
4353
4545
  * Integrated when steered, so a glide speeds the class up on the beat. */
4354
4546
  danceTempo: { type: "number", default: 128, min: 60, max: 180, ease: "smooth" },
4547
+ /** An authored dance on the beat (choreo.ts): a cue sheet per dancer —
4548
+ * `a: 8:lie, 4:rise, 4:reach >right @cam | *: 4:march ~360, 4:jacks ^0.5`.
4549
+ * Set, every starfish dances it (in the class's places, or the couples'
4550
+ * under `starfishDance: duet`), facing `cameraAzimuth`; a dancer it leaves
4551
+ * out keeps `starfishDance`'s routine, or stands and waits. Beats on the
4552
+ * `danceTempo` clock from the start, so a sheet lines up with music. */
4553
+ choreography: { type: "string", default: "", ease: "step" },
4355
4554
  /** Eye life, 0..1: blinks on a personal clock, idle saccades, pupils that
4356
4555
  * lead a turn or a climb, eyes on whoever a vignette has it talking to, a
4357
4556
  * glance at the camera now and then, wide for a hop and shut for a rest.
@@ -4751,6 +4950,10 @@ export {
4751
4950
  SIGN_TEXT_MAX,
4752
4951
  MAX_SIGNS,
4753
4952
  parseSignMix,
4953
+ STARFISH_MOVE_NAMES,
4954
+ parseChoreography,
4955
+ choreoLine,
4956
+ choreoAt,
4754
4957
  GESTURES,
4755
4958
  OPEN_MARKS,
4756
4959
  parseVignette,
@@ -4777,4 +4980,4 @@ export {
4777
4980
  paramSpaceWith,
4778
4981
  coerceNum
4779
4982
  };
4780
- //# sourceMappingURL=chunk-5DJ5WBVN.js.map
4983
+ //# sourceMappingURL=chunk-42RIVUT4.js.map