@idle-screens/saver-metaquarium 0.1.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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/chunk-4G3BQRZ3.js +102 -0
- package/dist/chunk-4G3BQRZ3.js.map +1 -0
- package/dist/chunk-YWDLFCBS.js +153 -0
- package/dist/chunk-YWDLFCBS.js.map +1 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +91 -0
- package/dist/manifest.js +17 -0
- package/dist/manifest.js.map +1 -0
- package/dist/tank-CQSFZYVF.js +508 -0
- package/dist/tank-CQSFZYVF.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shawn Partridge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @idle-screens/saver-metaquarium
|
|
2
|
+
|
|
3
|
+
A three.js aquarium saver: skinned GLB fish swim seeded Catmull-Rom spline
|
|
4
|
+
paths through a dark, fogged tank.
|
|
5
|
+
|
|
6
|
+
## Core animation loop
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
mount
|
|
10
|
+
├─ WebGLRenderer (stencil off, high-performance, sRGB, linear tone)
|
|
11
|
+
├─ Scene: fog (near 60, far 500) + flat MeshBasicMaterial floor
|
|
12
|
+
├─ PerspectiveCamera on param-steered spherical orbit
|
|
13
|
+
└─ populate():
|
|
14
|
+
for each fish index:
|
|
15
|
+
1. fetch + parse GLB → FishTemplate (module-level cache)
|
|
16
|
+
2. SkeletonUtils.clone() → per-fish skinned mesh
|
|
17
|
+
3. applyNpcMaterials(): seeded palette body + glow colors (all unlit)
|
|
18
|
+
4. compileSwimPlan(rng.fork(i), BOUNDS) → closed Catmull-Rom loop
|
|
19
|
+
with arc-length table + speed-wobble harmonics
|
|
20
|
+
5. add to scene
|
|
21
|
+
|
|
22
|
+
frame loop (rAF or renderFrame(t)):
|
|
23
|
+
1. governor: median frame time > 21ms → step render scale down 0.8×
|
|
24
|
+
2. setState(t):
|
|
25
|
+
- sample control track → live params
|
|
26
|
+
- camera orbit from cameraAzimuth + autoRotate * t
|
|
27
|
+
- fog color from fogColor param
|
|
28
|
+
- for each fish:
|
|
29
|
+
distance = distanceAt(plan, tSec, speed) // closed-form integral
|
|
30
|
+
pose = swimPoseAtDistance(plan, distance) // arc-length → spline param
|
|
31
|
+
group.position ← pose.xyz
|
|
32
|
+
group.lookAt ← pose.forward
|
|
33
|
+
group.rotateZ ← pose.roll (bank into turns)
|
|
34
|
+
mixer.setTime ← distance * 0.045 (tail beat)
|
|
35
|
+
3. renderer.render(scene, camera)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
- **Deterministic**: seeded RNG only, closed-form swim. `renderFrame(t, seed)`
|
|
41
|
+
is frame-addressable — same inputs, same frame.
|
|
42
|
+
- **Steerable**: camera, fish count, swim speed, fog color — all live via
|
|
43
|
+
control track.
|
|
44
|
+
- **Device-tiered**: `@idle-screens/capabilities` scales pixel ratio, AA, and
|
|
45
|
+
fish cap per device.
|
|
46
|
+
- **Adaptive governor**: steps render resolution down when frames exceed budget.
|
|
47
|
+
- **Zero-dep manifest subpath**: servers validate params without pulling three.js.
|
|
48
|
+
|
|
49
|
+
## Params
|
|
50
|
+
|
|
51
|
+
| Param | Type | Default | Description |
|
|
52
|
+
|-------|------|---------|-------------|
|
|
53
|
+
| cameraAzimuth | number | 35 | Orbit angle (degrees) |
|
|
54
|
+
| cameraElevation | number | 15 | Height angle above waterline |
|
|
55
|
+
| cameraDistance | number | 110 | Distance from tank center |
|
|
56
|
+
| autoRotate | number | 0 | Orbit speed (deg/s) |
|
|
57
|
+
| fishCount | number | 1 | Visible fish (step) |
|
|
58
|
+
| swimSpeed | number | 1 | Swim time-scale multiplier |
|
|
59
|
+
| fogColor | color | #030009 | Atmosphere / background |
|
|
60
|
+
| fishUrl | string | ipfs://…/fish_257_….glb | GLB model URL (playground can override to a local asset) |
|
|
61
|
+
|
|
62
|
+
## File map
|
|
63
|
+
|
|
64
|
+
| File | Lines | Role |
|
|
65
|
+
|------|-------|------|
|
|
66
|
+
| tank.ts | ~480 | Renderer, scene, fish spawn, setState, governor, dispose |
|
|
67
|
+
| plan.ts | ~180 | Catmull-Rom swim: compile, arc-length, pose-at-distance |
|
|
68
|
+
| materials.ts | ~90 | Seeded palette coat (unlit MeshBasicMaterial) |
|
|
69
|
+
| manifest.ts | ~100 | Param space, palettes, manifest metadata |
|
|
70
|
+
| quality.ts | ~35 | Device-tier quality caps |
|
|
71
|
+
| metaquarium.ts | ~40 | Plugin factory + demo track |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/manifest.ts
|
|
2
|
+
var METAQUARIUM_PARAMS = {
|
|
3
|
+
/** Camera orbit angle, degrees. */
|
|
4
|
+
cameraAzimuth: { type: "number", default: 35, min: 0, max: 360, ease: "smooth" },
|
|
5
|
+
/** Camera height angle, degrees above the waterline plane. */
|
|
6
|
+
cameraElevation: { type: "number", default: 15, min: -5, max: 60, ease: "smooth" },
|
|
7
|
+
/** Camera distance from tank center, world units. */
|
|
8
|
+
cameraDistance: { type: "number", default: 110, min: 80, max: 400, ease: "smooth" },
|
|
9
|
+
/** Continuous orbit speed, degrees/second. Zero by default: the tank is
|
|
10
|
+
* still, letting the fish movement carry the scene. Steer up for an orbit. */
|
|
11
|
+
autoRotate: { type: "number", default: 0, min: 0, max: 12, ease: "smooth" },
|
|
12
|
+
/** Visible fish. Default 1 = hero mode: one fish center-stage in its own
|
|
13
|
+
* tank; the pool grows on demand when steered up (never shrinks). */
|
|
14
|
+
fishCount: { type: "number", default: 1, min: 1, max: 24, ease: "step" },
|
|
15
|
+
/** Swim time-scale multiplier. */
|
|
16
|
+
swimSpeed: { type: "number", default: 1, min: 0.2, max: 3, ease: "smooth" },
|
|
17
|
+
/** Water/atmosphere color (background + fog). */
|
|
18
|
+
fogColor: { type: "color", default: "#030009", ease: "smooth" },
|
|
19
|
+
/** GLB model URL for the fish (supports ipfs:// scheme). Package default
|
|
20
|
+
* is the IPFS hero fish so npm / channel hosts work without playground
|
|
21
|
+
* static assets; the playground overrides this to a local GLB. */
|
|
22
|
+
fishUrl: { type: "string", default: "ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb" }
|
|
23
|
+
};
|
|
24
|
+
var MIAMI_VICE_COLORS = [
|
|
25
|
+
"#ff6ad5",
|
|
26
|
+
// Hot Pink
|
|
27
|
+
"#c774e8",
|
|
28
|
+
// Light Purple
|
|
29
|
+
"#ad8cff",
|
|
30
|
+
// Lavender
|
|
31
|
+
"#8795e8",
|
|
32
|
+
// Periwinkle Blue
|
|
33
|
+
"#94d0ff",
|
|
34
|
+
// Light Sky Blue
|
|
35
|
+
"#4fb4f4",
|
|
36
|
+
// Azure
|
|
37
|
+
"#00a6fb",
|
|
38
|
+
// Bright Blue
|
|
39
|
+
"#0085a1",
|
|
40
|
+
// Cerulean
|
|
41
|
+
"#0b3d91",
|
|
42
|
+
// Yale Blue
|
|
43
|
+
"#1c1c1c"
|
|
44
|
+
// Off Black
|
|
45
|
+
];
|
|
46
|
+
var BLOOM_COLORS = [
|
|
47
|
+
"#FF69B4",
|
|
48
|
+
// Hot Pink
|
|
49
|
+
"#FFFF00",
|
|
50
|
+
// Yellow
|
|
51
|
+
"#00FFFF",
|
|
52
|
+
// Cyan
|
|
53
|
+
"#00FF00",
|
|
54
|
+
// Lime
|
|
55
|
+
"#FF4500",
|
|
56
|
+
// OrangeRed
|
|
57
|
+
"#7FFF00"
|
|
58
|
+
// Chartreuse
|
|
59
|
+
];
|
|
60
|
+
var metaquariumManifest = {
|
|
61
|
+
id: "metaquarium",
|
|
62
|
+
label: "Metaquarium",
|
|
63
|
+
description: "A living fish tank: skinned GLB fish swim seeded Catmull-Rom paths through a dark, fogged aquarium.",
|
|
64
|
+
minBackend: "webgl2",
|
|
65
|
+
costTier: "medium",
|
|
66
|
+
motionIntensity: "calm",
|
|
67
|
+
reducedMotionFallback: "static",
|
|
68
|
+
timeModel: "closed-form",
|
|
69
|
+
palette: ["#030009", "#ff6ad5", "#94d0ff", "#00a6fb", "#00FFFF"],
|
|
70
|
+
paramSpace: METAQUARIUM_PARAMS,
|
|
71
|
+
attribution: {
|
|
72
|
+
source: "Metaquarium (metaquarium.xyz) \u2014 original artwork and fish models by Shawn Partridge",
|
|
73
|
+
license: "MIT port of first-party artwork",
|
|
74
|
+
url: "https://metaquarium.xyz"
|
|
75
|
+
},
|
|
76
|
+
a11y: {
|
|
77
|
+
flashSafe: true,
|
|
78
|
+
notes: "Slow ambient swim in a dark fogged tank; no strobing."
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function withDefaults(space, overrides) {
|
|
82
|
+
if (!overrides) return space;
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const [k, def] of Object.entries(space)) {
|
|
85
|
+
const o = overrides[k];
|
|
86
|
+
out[k] = o === void 0 ? def : { ...def, default: o };
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function paramSpaceWith(overrides) {
|
|
91
|
+
return withDefaults(METAQUARIUM_PARAMS, overrides);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
METAQUARIUM_PARAMS,
|
|
96
|
+
MIAMI_VICE_COLORS,
|
|
97
|
+
BLOOM_COLORS,
|
|
98
|
+
metaquariumManifest,
|
|
99
|
+
withDefaults,
|
|
100
|
+
paramSpaceWith
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=chunk-4G3BQRZ3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/manifest.ts"],"sourcesContent":["import type { ParamSpace, ParamValue, SaverManifest } from '@idle-screens/core';\n\n/**\n * Zero-dependency manifest module (type-only imports). The channel server\n * imports `@idle-screens/saver-metaquarium/manifest` to validate published\n * params against this paramSpace without pulling three.js into the Worker.\n */\nexport const METAQUARIUM_PARAMS = {\n /** Camera orbit angle, degrees. */\n cameraAzimuth: { type: 'number', default: 35, min: 0, max: 360, ease: 'smooth' },\n /** Camera height angle, degrees above the waterline plane. */\n cameraElevation: { type: 'number', default: 15, min: -5, max: 60, ease: 'smooth' },\n /** Camera distance from tank center, world units. */\n cameraDistance: { type: 'number', default: 110, min: 80, max: 400, ease: 'smooth' },\n /** Continuous orbit speed, degrees/second. Zero by default: the tank is\n * still, letting the fish movement carry the scene. Steer up for an orbit. */\n autoRotate: { type: 'number', default: 0, min: 0, max: 12, ease: 'smooth' },\n /** Visible fish. Default 1 = hero mode: one fish center-stage in its own\n * tank; the pool grows on demand when steered up (never shrinks). */\n fishCount: { type: 'number', default: 1, min: 1, max: 24, ease: 'step' },\n /** Swim time-scale multiplier. */\n swimSpeed: { type: 'number', default: 1, min: 0.2, max: 3, ease: 'smooth' },\n /** Water/atmosphere color (background + fog). */\n fogColor: { type: 'color', default: '#030009', ease: 'smooth' },\n /** GLB model URL for the fish (supports ipfs:// scheme). Package default\n * is the IPFS hero fish so npm / channel hosts work without playground\n * static assets; the playground overrides this to a local GLB. */\n fishUrl: { type: 'string', default: 'ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb' },\n} satisfies ParamSpace;\n\n/** The original's Miami-Vice body palette (scss-variables.ts) — seeded fish\n * coats draw from this. Zero-dep so the server/manifest lane can surface it. */\nexport const MIAMI_VICE_COLORS = [\n '#ff6ad5', // Hot Pink\n '#c774e8', // Light Purple\n '#ad8cff', // Lavender\n '#8795e8', // Periwinkle Blue\n '#94d0ff', // Light Sky Blue\n '#4fb4f4', // Azure\n '#00a6fb', // Bright Blue\n '#0085a1', // Cerulean\n '#0b3d91', // Yale Blue\n '#1c1c1c', // Off Black\n];\n\n/** The original's emissive/glow palette (`GLOW-*` parts, bloom layer). */\nexport const BLOOM_COLORS = [\n '#FF69B4', // Hot Pink\n '#FFFF00', // Yellow\n '#00FFFF', // Cyan\n '#00FF00', // Lime\n '#FF4500', // OrangeRed\n '#7FFF00', // Chartreuse\n];\n\nexport const metaquariumManifest: SaverManifest = {\n id: 'metaquarium',\n label: 'Metaquarium',\n description:\n 'A living fish tank: skinned GLB fish swim seeded Catmull-Rom paths through a dark, fogged aquarium.',\n minBackend: 'webgl2',\n costTier: 'medium',\n motionIntensity: 'calm',\n reducedMotionFallback: 'static',\n timeModel: 'closed-form',\n palette: ['#030009', '#ff6ad5', '#94d0ff', '#00a6fb', '#00FFFF'],\n paramSpace: METAQUARIUM_PARAMS,\n attribution: {\n source: 'Metaquarium (metaquarium.xyz) — original artwork and fish models by Shawn Partridge',\n license: 'MIT port of first-party artwork',\n url: 'https://metaquarium.xyz',\n },\n a11y: {\n flashSafe: true,\n notes: 'Slow ambient swim in a dark fogged tank; no strobing.',\n },\n};\n\n/** Options for {@link createMetaquarium}-style variants: a distinct id/label and\n * overridden param defaults (e.g. a farm-connected tank). */\nexport interface MetaquariumOptions {\n id?: string;\n label?: string;\n params?: Partial<Record<keyof typeof METAQUARIUM_PARAMS, ParamValue>>;\n}\n\n/** Clone a paramSpace with default overrides; unknown keys are ignored. */\nexport function withDefaults(\n space: ParamSpace,\n overrides?: Record<string, ParamValue>,\n): ParamSpace {\n if (!overrides) return space;\n const out: ParamSpace = {};\n for (const [k, def] of Object.entries(space)) {\n const o = overrides[k];\n out[k] = o === undefined ? def : { ...def, default: o };\n }\n return out;\n}\n\n/** Clone the metaquarium paramSpace with per-variant default overrides. */\nexport function paramSpaceWith(overrides: MetaquariumOptions['params']): ParamSpace {\n return withDefaults(METAQUARIUM_PARAMS, overrides as Record<string, ParamValue> | undefined);\n}\n"],"mappings":";AAOO,IAAM,qBAAqB;AAAA;AAAA,EAEhC,eAAe,EAAE,MAAM,UAAU,SAAS,IAAI,KAAK,GAAG,KAAK,KAAK,MAAM,SAAS;AAAA;AAAA,EAE/E,iBAAiB,EAAE,MAAM,UAAU,SAAS,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM,SAAS;AAAA;AAAA,EAEjF,gBAAgB,EAAE,MAAM,UAAU,SAAS,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,SAAS;AAAA;AAAA;AAAA,EAGlF,YAAY,EAAE,MAAM,UAAU,SAAS,GAAG,KAAK,GAAG,KAAK,IAAI,MAAM,SAAS;AAAA;AAAA;AAAA,EAG1E,WAAW,EAAE,MAAM,UAAU,SAAS,GAAG,KAAK,GAAG,KAAK,IAAI,MAAM,OAAO;AAAA;AAAA,EAEvE,WAAW,EAAE,MAAM,UAAU,SAAS,GAAG,KAAK,KAAK,KAAK,GAAG,MAAM,SAAS;AAAA;AAAA,EAE1E,UAAU,EAAE,MAAM,SAAS,SAAS,WAAW,MAAM,SAAS;AAAA;AAAA;AAAA;AAAA,EAI9D,SAAS,EAAE,MAAM,UAAU,SAAS,2FAA2F;AACjI;AAIO,IAAM,oBAAoB;AAAA,EAC/B;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF;AAGO,IAAM,eAAe;AAAA,EAC1B;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF;AAEO,IAAM,sBAAqC;AAAA,EAChD,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aACE;AAAA,EACF,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB,uBAAuB;AAAA,EACvB,WAAW;AAAA,EACX,SAAS,CAAC,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,EAC/D,YAAY;AAAA,EACZ,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,KAAK;AAAA,EACP;AAAA,EACA,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,OAAO;AAAA,EACT;AACF;AAWO,SAAS,aACd,OACA,WACY;AACZ,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,MAAkB,CAAC;AACzB,aAAW,CAAC,GAAG,GAAG,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC5C,UAAM,IAAI,UAAU,CAAC;AACrB,QAAI,CAAC,IAAI,MAAM,SAAY,MAAM,EAAE,GAAG,KAAK,SAAS,EAAE;AAAA,EACxD;AACA,SAAO;AACT;AAGO,SAAS,eAAe,WAAqD;AAClF,SAAO,aAAa,oBAAoB,SAAmD;AAC7F;","names":[]}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/ipfs.ts
|
|
2
|
+
var IPFS_GATEWAY = "https://dweb.link/ipfs/";
|
|
3
|
+
function resolveIpfsUrl(url) {
|
|
4
|
+
if (!url.startsWith("ipfs://")) return url;
|
|
5
|
+
const stripped = url.slice("ipfs://".length);
|
|
6
|
+
return `${IPFS_GATEWAY}${stripped}`;
|
|
7
|
+
}
|
|
8
|
+
var FISH_CATALOG = [
|
|
9
|
+
{ id: 257, name: "Fish 257", breed: "angelfish", ipfs3d: "ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb", localGlb: "/assets/metaquarium/fish-257-angelfish.glb" },
|
|
10
|
+
{ id: 258, name: "Fish 258", breed: "angelfish", ipfs3d: "ipfs://QmUZGF3ge3d9rzrtxrD6V4qx2gLtGeeNLuCb8fQeNyUkwJ/fish_258_of_the_metaquarium_3d.glb", localGlb: "" },
|
|
11
|
+
{ id: 259, name: "Fish 259", breed: "angelfish", ipfs3d: "ipfs://QmfBBnNrVrkffMKoESvq3cB6nAWGpfMPjduTgw1unahvPf/fish_259_of_the_metaquarium_3d.glb", localGlb: "" },
|
|
12
|
+
{ id: 100, name: "Fish 100", breed: "betafish", ipfs3d: "ipfs://Qmb5Uu8u154QTzoGpB6ypwVfPZ8NUsU519tQmrgE8yQrWV/fish_100_of_the_metaquarium_3d.glb", localGlb: "/assets/metaquarium/fish-100-betafish.glb" },
|
|
13
|
+
{ id: 457, name: "Fish 457", breed: "seahorse", ipfs3d: "ipfs://QmVvEaCa6zRp8Z9YkkZVYBn2owSdwZxupEQacjfd1b2HA2/fish_457_of_the_metaquarium_3d.glb", localGlb: "" },
|
|
14
|
+
{ id: 497, name: "Fish 497", breed: "seaturtle", ipfs3d: "ipfs://QmTBNvoUiwPw9HSUmy1qKCWPBKkBRAensgooYVqMmsviaE/fish_497_of_the_metaquarium_3d.glb", localGlb: "" }
|
|
15
|
+
];
|
|
16
|
+
var DEFAULT_FISH = FISH_CATALOG[0];
|
|
17
|
+
|
|
18
|
+
// src/plan.ts
|
|
19
|
+
var ARC_SAMPLES = 1024;
|
|
20
|
+
function catmullRom(p0, p1, p2, p3, u) {
|
|
21
|
+
const u2 = u * u;
|
|
22
|
+
const u3 = u2 * u;
|
|
23
|
+
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
24
|
+
}
|
|
25
|
+
function splineAt(points, g) {
|
|
26
|
+
const n = points.length;
|
|
27
|
+
const i = Math.floor(g) % n;
|
|
28
|
+
const u = g - Math.floor(g);
|
|
29
|
+
const p0 = points[(i - 1 + n) % n];
|
|
30
|
+
const p1 = points[i];
|
|
31
|
+
const p2 = points[(i + 1) % n];
|
|
32
|
+
const p3 = points[(i + 2) % n];
|
|
33
|
+
return [
|
|
34
|
+
catmullRom(p0[0], p1[0], p2[0], p3[0], u),
|
|
35
|
+
catmullRom(p0[1], p1[1], p2[1], p3[1], u),
|
|
36
|
+
catmullRom(p0[2], p1[2], p2[2], p3[2], u)
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
function compileSwimPlan(rng, bounds) {
|
|
40
|
+
const n = 16 + rng.int(0, 4);
|
|
41
|
+
const points = [];
|
|
42
|
+
const ySpan = bounds.yMax - bounds.yMin;
|
|
43
|
+
for (let i = 0; i < n; i++) {
|
|
44
|
+
const baseAngle = i / n * Math.PI * 2;
|
|
45
|
+
const angle = baseAngle + rng.range(-0.25, 0.25);
|
|
46
|
+
const inner = i % 2 === 0;
|
|
47
|
+
const radius = inner ? bounds.radius * rng.range(0.15, 0.45) : bounds.radius * rng.range(0.55, 0.85);
|
|
48
|
+
const y = bounds.yMin + ySpan * rng.range(0.2, 0.8);
|
|
49
|
+
points.push([Math.cos(angle) * radius, y, Math.sin(angle) * radius]);
|
|
50
|
+
}
|
|
51
|
+
const arc = new Float32Array(ARC_SAMPLES + 1);
|
|
52
|
+
let prev = splineAt(points, 0);
|
|
53
|
+
let total = 0;
|
|
54
|
+
for (let s = 1; s <= ARC_SAMPLES; s++) {
|
|
55
|
+
const g = s / ARC_SAMPLES * n;
|
|
56
|
+
const p = splineAt(points, g);
|
|
57
|
+
total += Math.hypot(p[0] - prev[0], p[1] - prev[1], p[2] - prev[2]);
|
|
58
|
+
arc[s] = total;
|
|
59
|
+
prev = p;
|
|
60
|
+
}
|
|
61
|
+
const cruise = 8 + rng.range(0, 4);
|
|
62
|
+
const wobble = [
|
|
63
|
+
{ amp: cruise * 0.18, w: 0.11 + rng.range(0, 0.05), phase: rng.range(0, Math.PI * 2) },
|
|
64
|
+
{ amp: cruise * 0.1, w: 0.043 + rng.range(0, 0.02), phase: rng.range(0, Math.PI * 2) }
|
|
65
|
+
];
|
|
66
|
+
return { points, arc, totalLength: total, cruise, wobble };
|
|
67
|
+
}
|
|
68
|
+
function distanceAt(plan, tSec, speed) {
|
|
69
|
+
let d = plan.cruise * tSec;
|
|
70
|
+
for (const h of plan.wobble) {
|
|
71
|
+
d += h.amp / h.w * (Math.cos(h.phase) - Math.cos(h.w * tSec + h.phase));
|
|
72
|
+
}
|
|
73
|
+
return d * speed;
|
|
74
|
+
}
|
|
75
|
+
function paramForDistance(plan, dist) {
|
|
76
|
+
const n = plan.points.length;
|
|
77
|
+
const d = (dist % plan.totalLength + plan.totalLength) % plan.totalLength;
|
|
78
|
+
const arc = plan.arc;
|
|
79
|
+
let lo = 0;
|
|
80
|
+
let hi = ARC_SAMPLES;
|
|
81
|
+
while (lo + 1 < hi) {
|
|
82
|
+
const mid = lo + hi >> 1;
|
|
83
|
+
if (arc[mid] <= d) lo = mid;
|
|
84
|
+
else hi = mid;
|
|
85
|
+
}
|
|
86
|
+
const seg = arc[hi] - arc[lo] || 1;
|
|
87
|
+
const frac = (d - arc[lo]) / seg;
|
|
88
|
+
return (lo + frac) / ARC_SAMPLES * n;
|
|
89
|
+
}
|
|
90
|
+
function swimPoseAtDistance(plan, dist) {
|
|
91
|
+
const g = paramForDistance(plan, dist);
|
|
92
|
+
const [x, y, z] = splineAt(plan.points, g);
|
|
93
|
+
const step = plan.points.length / ARC_SAMPLES * 8;
|
|
94
|
+
const ahead = splineAt(plan.points, g + step);
|
|
95
|
+
const behind = splineAt(plan.points, g - step + plan.points.length);
|
|
96
|
+
let fx = ahead[0] - behind[0];
|
|
97
|
+
let fy = ahead[1] - behind[1];
|
|
98
|
+
let fz = ahead[2] - behind[2];
|
|
99
|
+
const m = Math.hypot(fx, fy, fz) || 1;
|
|
100
|
+
fx /= m;
|
|
101
|
+
fy /= m;
|
|
102
|
+
fz /= m;
|
|
103
|
+
const h0 = Math.atan2(z - behind[2], x - behind[0]);
|
|
104
|
+
const h1 = Math.atan2(ahead[2] - z, ahead[0] - x);
|
|
105
|
+
let turn = h1 - h0;
|
|
106
|
+
if (turn > Math.PI) turn -= Math.PI * 2;
|
|
107
|
+
if (turn < -Math.PI) turn += Math.PI * 2;
|
|
108
|
+
const roll = Math.max(-0.35, Math.min(0.35, turn * 3.5));
|
|
109
|
+
return { x, y, z, fx, fy, fz, roll, dist };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/quality.ts
|
|
113
|
+
function qualityFor(tier) {
|
|
114
|
+
switch (tier) {
|
|
115
|
+
case "high":
|
|
116
|
+
return { maxPixelRatio: 1.25, antialias: true, fishCap: 24, pixelBudget: 18e5 };
|
|
117
|
+
case "standard":
|
|
118
|
+
return { maxPixelRatio: 1, antialias: true, fishCap: 16, pixelBudget: 12e5 };
|
|
119
|
+
default:
|
|
120
|
+
return { maxPixelRatio: 1, antialias: false, fishCap: 8, pixelBudget: 9e5 };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function effectivePixelRatio(width, height, dpr, q) {
|
|
124
|
+
const area = Math.max(1, width * height);
|
|
125
|
+
return Math.max(0.5, Math.min(dpr, q.maxPixelRatio, Math.sqrt(q.pixelBudget / area)));
|
|
126
|
+
}
|
|
127
|
+
function isSoftwareGL(renderer) {
|
|
128
|
+
return /swiftshader|llvmpipe|software|swangle/i.test(renderer);
|
|
129
|
+
}
|
|
130
|
+
function probeSoftwareGL() {
|
|
131
|
+
if (typeof document === "undefined") return false;
|
|
132
|
+
const canvas = document.createElement("canvas");
|
|
133
|
+
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
|
134
|
+
if (!gl) return false;
|
|
135
|
+
const dbg = gl.getExtension("WEBGL_debug_renderer_info");
|
|
136
|
+
const renderer = String(
|
|
137
|
+
dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER)
|
|
138
|
+
);
|
|
139
|
+
return isSoftwareGL(renderer);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
resolveIpfsUrl,
|
|
144
|
+
FISH_CATALOG,
|
|
145
|
+
DEFAULT_FISH,
|
|
146
|
+
compileSwimPlan,
|
|
147
|
+
distanceAt,
|
|
148
|
+
swimPoseAtDistance,
|
|
149
|
+
qualityFor,
|
|
150
|
+
effectivePixelRatio,
|
|
151
|
+
probeSoftwareGL
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=chunk-YWDLFCBS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ipfs.ts","../src/plan.ts","../src/quality.ts"],"sourcesContent":["const IPFS_GATEWAY = 'https://dweb.link/ipfs/';\n\nexport function resolveIpfsUrl(url: string): string {\n if (!url.startsWith('ipfs://')) return url;\n const stripped = url.slice('ipfs://'.length);\n return `${IPFS_GATEWAY}${stripped}`;\n}\n\nexport interface FishEntry {\n id: number;\n name: string;\n breed: string;\n ipfs3d: string;\n localGlb: string;\n}\n\nexport const FISH_CATALOG: FishEntry[] = [\n { id: 257, name: 'Fish 257', breed: 'angelfish', ipfs3d: 'ipfs://QmaHbEQAP6k2zopJHJBzyaK62zNX5yH8yASDjkaG4DY9Dp/fish_257_of_the_metaquarium_3d.glb', localGlb: '/assets/metaquarium/fish-257-angelfish.glb' },\n { id: 258, name: 'Fish 258', breed: 'angelfish', ipfs3d: 'ipfs://QmUZGF3ge3d9rzrtxrD6V4qx2gLtGeeNLuCb8fQeNyUkwJ/fish_258_of_the_metaquarium_3d.glb', localGlb: '' },\n { id: 259, name: 'Fish 259', breed: 'angelfish', ipfs3d: 'ipfs://QmfBBnNrVrkffMKoESvq3cB6nAWGpfMPjduTgw1unahvPf/fish_259_of_the_metaquarium_3d.glb', localGlb: '' },\n { id: 100, name: 'Fish 100', breed: 'betafish', ipfs3d: 'ipfs://Qmb5Uu8u154QTzoGpB6ypwVfPZ8NUsU519tQmrgE8yQrWV/fish_100_of_the_metaquarium_3d.glb', localGlb: '/assets/metaquarium/fish-100-betafish.glb' },\n { id: 457, name: 'Fish 457', breed: 'seahorse', ipfs3d: 'ipfs://QmVvEaCa6zRp8Z9YkkZVYBn2owSdwZxupEQacjfd1b2HA2/fish_457_of_the_metaquarium_3d.glb', localGlb: '' },\n { id: 497, name: 'Fish 497', breed: 'seaturtle', ipfs3d: 'ipfs://QmTBNvoUiwPw9HSUmy1qKCWPBKkBRAensgooYVqMmsviaE/fish_497_of_the_metaquarium_3d.glb', localGlb: '' },\n];\n\nexport const DEFAULT_FISH = FISH_CATALOG[0]!;\n","import type { Rng } from '@idle-screens/core';\n\nexport interface TankBounds {\n radius: number;\n yMin: number;\n yMax: number;\n}\n\n/**\n * Catmull-Rom spline swim: a seeded closed loop of waypoints with arc-length\n * parameterization. Pose is a pure function of (plan, distance), so\n * `renderFrame(t, seed)` stays frame-addressable and scrubbing works.\n */\n\nexport interface SwimPlan {\n /** Closed-loop control points. */\n points: Array<[number, number, number]>;\n /** Arc-length lookup: cumulative length at N uniform spline params. */\n arc: Float32Array;\n totalLength: number;\n /** Cruise speed, units/sec, before the swimSpeed param scales it. */\n cruise: number;\n /** Speed-wobble harmonics (closed-form integral → distance). */\n wobble: Array<{ amp: number; w: number; phase: number }>;\n}\n\nexport interface SwimPose {\n x: number;\n y: number;\n z: number;\n /** Unit forward tangent — the fish noses along this. */\n fx: number;\n fy: number;\n fz: number;\n /** Bank roll (radians): lean into horizontal turns. */\n roll: number;\n /** Distance travelled (drives the tail-beat clip phase). */\n dist: number;\n}\n\nconst ARC_SAMPLES = 1024;\n\nfunction catmullRom(\n p0: number,\n p1: number,\n p2: number,\n p3: number,\n u: number,\n): number {\n const u2 = u * u;\n const u3 = u2 * u;\n return (\n 0.5 *\n (2 * p1 +\n (-p0 + p2) * u +\n (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 +\n (-p0 + 3 * p1 - 3 * p2 + p3) * u3)\n );\n}\n\n/** Point on the closed loop at global param `g` ∈ [0, n). */\nfunction splineAt(points: SwimPlan['points'], g: number): [number, number, number] {\n const n = points.length;\n const i = Math.floor(g) % n;\n const u = g - Math.floor(g);\n const p0 = points[(i - 1 + n) % n]!;\n const p1 = points[i]!;\n const p2 = points[(i + 1) % n]!;\n const p3 = points[(i + 2) % n]!;\n return [\n catmullRom(p0[0], p1[0], p2[0], p3[0], u),\n catmullRom(p0[1], p1[1], p2[1], p3[1], u),\n catmullRom(p0[2], p1[2], p2[2], p3[2], u),\n ];\n}\n\n/**\n * Compile a seeded itinerary through the tank volume. Waypoints alternate\n * around the ring (angle jitter, radius breathing, depth changes) so the\n * loop reads as purposeful wandering, never a circle.\n */\nexport function compileSwimPlan(rng: Rng, bounds: TankBounds): SwimPlan {\n const n = 16 + rng.int(0, 4);\n const points: SwimPlan['points'] = [];\n const ySpan = bounds.yMax - bounds.yMin;\n for (let i = 0; i < n; i++) {\n const baseAngle = (i / n) * Math.PI * 2;\n const angle = baseAngle + rng.range(-0.25, 0.25);\n const inner = i % 2 === 0;\n const radius = inner\n ? bounds.radius * rng.range(0.15, 0.45)\n : bounds.radius * rng.range(0.55, 0.85);\n const y = bounds.yMin + ySpan * rng.range(0.2, 0.8);\n points.push([Math.cos(angle) * radius, y, Math.sin(angle) * radius]);\n }\n\n // Arc-length table over the closed loop.\n const arc = new Float32Array(ARC_SAMPLES + 1);\n let prev = splineAt(points, 0);\n let total = 0;\n for (let s = 1; s <= ARC_SAMPLES; s++) {\n const g = (s / ARC_SAMPLES) * n;\n const p = splineAt(points, g);\n total += Math.hypot(p[0] - prev[0], p[1] - prev[1], p[2] - prev[2]);\n arc[s] = total;\n prev = p;\n }\n\n // Cruise + gentle speed wobble: sometimes dawdling, sometimes keen — the\n // integral of each harmonic is closed-form, so distance(t) never drifts.\n const cruise = 8 + rng.range(0, 4);\n const wobble = [\n { amp: cruise * 0.18, w: 0.11 + rng.range(0, 0.05), phase: rng.range(0, Math.PI * 2) },\n { amp: cruise * 0.1, w: 0.043 + rng.range(0, 0.02), phase: rng.range(0, Math.PI * 2) },\n ];\n\n return { points, arc, totalLength: total, cruise, wobble };\n}\n\n/** Distance travelled by time `tSec` at `speed` scale — pure closed form. */\nexport function distanceAt(plan: SwimPlan, tSec: number, speed: number): number {\n let d = plan.cruise * tSec;\n for (const h of plan.wobble) {\n // ∫ amp·sin(w·t + φ) dt = −(amp/w)·cos(w·t + φ); anchored so d(0) = 0.\n d += (h.amp / h.w) * (Math.cos(h.phase) - Math.cos(h.w * tSec + h.phase));\n }\n return d * speed;\n}\n\n/** Global spline param for a given arc distance (binary search + lerp). */\nfunction paramForDistance(plan: SwimPlan, dist: number): number {\n const n = plan.points.length;\n const d = ((dist % plan.totalLength) + plan.totalLength) % plan.totalLength;\n const arc = plan.arc;\n let lo = 0;\n let hi = ARC_SAMPLES;\n while (lo + 1 < hi) {\n const mid = (lo + hi) >> 1;\n if (arc[mid]! <= d) lo = mid;\n else hi = mid;\n }\n const seg = arc[hi]! - arc[lo]! || 1;\n const frac = (d - arc[lo]!) / seg;\n return ((lo + frac) / ARC_SAMPLES) * n;\n}\n\n/** Pose at `tSec` — position, unit forward tangent, bank roll, distance. */\nexport function swimPoseAt(plan: SwimPlan, tSec: number, speed = 1): SwimPose {\n return swimPoseAtDistance(plan, distanceAt(plan, tSec, speed));\n}\n\n/** Pose at an explicit arc distance — the seam for behavior-modulated speed\n * (the tank integrates distance so speed changes glide instead of\n * teleporting; scrubs reset to the closed-form distance). */\nexport function swimPoseAtDistance(plan: SwimPlan, dist: number): SwimPose {\n const g = paramForDistance(plan, dist);\n const [x, y, z] = splineAt(plan.points, g);\n\n const step = (plan.points.length / ARC_SAMPLES) * 8;\n const ahead = splineAt(plan.points, g + step);\n const behind = splineAt(plan.points, g - step + plan.points.length);\n let fx = ahead[0] - behind[0];\n let fy = ahead[1] - behind[1];\n let fz = ahead[2] - behind[2];\n const m = Math.hypot(fx, fy, fz) || 1;\n fx /= m;\n fy /= m;\n fz /= m;\n\n // Bank: signed change of horizontal heading over the step → lean into it.\n const h0 = Math.atan2(z - behind[2], x - behind[0]);\n const h1 = Math.atan2(ahead[2] - z, ahead[0] - x);\n let turn = h1 - h0;\n if (turn > Math.PI) turn -= Math.PI * 2;\n if (turn < -Math.PI) turn += Math.PI * 2;\n const roll = Math.max(-0.35, Math.min(0.35, turn * 3.5));\n\n return { x, y, z, fx, fy, fz, roll, dist };\n}\n","import type { CapabilityTier } from '@idle-screens/capabilities';\n\nexport interface TankQuality {\n maxPixelRatio: number;\n antialias: boolean;\n fishCap: number;\n pixelBudget: number;\n}\n\nexport function qualityFor(tier: CapabilityTier): TankQuality {\n switch (tier) {\n case 'high':\n return { maxPixelRatio: 1.25, antialias: true, fishCap: 24, pixelBudget: 1_800_000 };\n case 'standard':\n return { maxPixelRatio: 1, antialias: true, fishCap: 16, pixelBudget: 1_200_000 };\n default:\n return { maxPixelRatio: 1, antialias: false, fishCap: 8, pixelBudget: 900_000 };\n }\n}\n\nexport function effectivePixelRatio(\n width: number,\n height: number,\n dpr: number,\n q: TankQuality,\n): number {\n const area = Math.max(1, width * height);\n return Math.max(0.5, Math.min(dpr, q.maxPixelRatio, Math.sqrt(q.pixelBudget / area)));\n}\n\nexport function isSoftwareGL(renderer: string): boolean {\n return /swiftshader|llvmpipe|software|swangle/i.test(renderer);\n}\n\n/** Probe the default GL context for a software renderer before creating a\n * WebGLRenderer so antialias/quality tier match from the first frame. */\nexport function probeSoftwareGL(): boolean {\n if (typeof document === 'undefined') return false;\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');\n if (!gl) return false;\n const dbg = gl.getExtension('WEBGL_debug_renderer_info');\n const renderer = String(\n dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),\n );\n return isSoftwareGL(renderer);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SaverPlugin, ControlTrack } from '@idle-screens/core';
|
|
2
|
+
import { MetaquariumOptions } from './manifest.js';
|
|
3
|
+
export { BLOOM_COLORS, METAQUARIUM_PARAMS, MIAMI_VICE_COLORS, metaquariumManifest, paramSpaceWith, withDefaults } from './manifest.js';
|
|
4
|
+
import { CapabilityTier } from '@idle-screens/capabilities';
|
|
5
|
+
|
|
6
|
+
declare function createMetaquarium(opts?: MetaquariumOptions): SaverPlugin;
|
|
7
|
+
declare const metaquarium: SaverPlugin;
|
|
8
|
+
declare const demoTrack: ControlTrack;
|
|
9
|
+
|
|
10
|
+
interface TankQuality {
|
|
11
|
+
maxPixelRatio: number;
|
|
12
|
+
antialias: boolean;
|
|
13
|
+
fishCap: number;
|
|
14
|
+
pixelBudget: number;
|
|
15
|
+
}
|
|
16
|
+
declare function qualityFor(tier: CapabilityTier): TankQuality;
|
|
17
|
+
|
|
18
|
+
interface TankBounds {
|
|
19
|
+
radius: number;
|
|
20
|
+
yMin: number;
|
|
21
|
+
yMax: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Catmull-Rom spline swim: a seeded closed loop of waypoints with arc-length
|
|
25
|
+
* parameterization. Pose is a pure function of (plan, distance), so
|
|
26
|
+
* `renderFrame(t, seed)` stays frame-addressable and scrubbing works.
|
|
27
|
+
*/
|
|
28
|
+
interface SwimPlan {
|
|
29
|
+
/** Closed-loop control points. */
|
|
30
|
+
points: Array<[number, number, number]>;
|
|
31
|
+
/** Arc-length lookup: cumulative length at N uniform spline params. */
|
|
32
|
+
arc: Float32Array;
|
|
33
|
+
totalLength: number;
|
|
34
|
+
/** Cruise speed, units/sec, before the swimSpeed param scales it. */
|
|
35
|
+
cruise: number;
|
|
36
|
+
/** Speed-wobble harmonics (closed-form integral → distance). */
|
|
37
|
+
wobble: Array<{
|
|
38
|
+
amp: number;
|
|
39
|
+
w: number;
|
|
40
|
+
phase: number;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
interface SwimPose {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
z: number;
|
|
47
|
+
/** Unit forward tangent — the fish noses along this. */
|
|
48
|
+
fx: number;
|
|
49
|
+
fy: number;
|
|
50
|
+
fz: number;
|
|
51
|
+
/** Bank roll (radians): lean into horizontal turns. */
|
|
52
|
+
roll: number;
|
|
53
|
+
/** Distance travelled (drives the tail-beat clip phase). */
|
|
54
|
+
dist: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare function resolveIpfsUrl(url: string): string;
|
|
58
|
+
interface FishEntry {
|
|
59
|
+
id: number;
|
|
60
|
+
name: string;
|
|
61
|
+
breed: string;
|
|
62
|
+
ipfs3d: string;
|
|
63
|
+
localGlb: string;
|
|
64
|
+
}
|
|
65
|
+
declare const FISH_CATALOG: FishEntry[];
|
|
66
|
+
declare const DEFAULT_FISH: FishEntry;
|
|
67
|
+
|
|
68
|
+
export { DEFAULT_FISH, FISH_CATALOG, type FishEntry, MetaquariumOptions, type SwimPlan, type SwimPose, type TankBounds, type TankQuality, createMetaquarium, demoTrack, metaquarium, qualityFor, resolveIpfsUrl };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_FISH,
|
|
3
|
+
FISH_CATALOG,
|
|
4
|
+
qualityFor,
|
|
5
|
+
resolveIpfsUrl
|
|
6
|
+
} from "./chunk-YWDLFCBS.js";
|
|
7
|
+
import {
|
|
8
|
+
BLOOM_COLORS,
|
|
9
|
+
METAQUARIUM_PARAMS,
|
|
10
|
+
MIAMI_VICE_COLORS,
|
|
11
|
+
metaquariumManifest,
|
|
12
|
+
paramSpaceWith,
|
|
13
|
+
withDefaults
|
|
14
|
+
} from "./chunk-4G3BQRZ3.js";
|
|
15
|
+
|
|
16
|
+
// src/metaquarium.ts
|
|
17
|
+
import { detectCapabilities, computeTier } from "@idle-screens/capabilities";
|
|
18
|
+
function createMetaquarium(opts = {}) {
|
|
19
|
+
const space = paramSpaceWith(opts.params);
|
|
20
|
+
return {
|
|
21
|
+
manifest: {
|
|
22
|
+
...metaquariumManifest,
|
|
23
|
+
id: opts.id ?? metaquariumManifest.id,
|
|
24
|
+
label: opts.label ?? metaquariumManifest.label,
|
|
25
|
+
paramSpace: space
|
|
26
|
+
},
|
|
27
|
+
async mount(ctx) {
|
|
28
|
+
const [{ mountTank }, caps] = await Promise.all([
|
|
29
|
+
import("./tank-CQSFZYVF.js"),
|
|
30
|
+
detectCapabilities()
|
|
31
|
+
]);
|
|
32
|
+
return mountTank(ctx, space, computeTier(caps));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var metaquarium = createMetaquarium();
|
|
37
|
+
var demoTrack = {
|
|
38
|
+
program: "metaquarium",
|
|
39
|
+
seed: 42,
|
|
40
|
+
duration: 3e4,
|
|
41
|
+
loop: true,
|
|
42
|
+
deltas: [
|
|
43
|
+
{ t: 5e3, path: "autoRotate", value: 3, dur: 4e3 },
|
|
44
|
+
{ t: 15e3, path: "fogColor", value: "#04002c", dur: 5e3 },
|
|
45
|
+
{ t: 22e3, path: "fogColor", value: "#030009", dur: 5e3 },
|
|
46
|
+
{ t: 25e3, path: "autoRotate", value: 0, dur: 4e3 }
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
export {
|
|
50
|
+
BLOOM_COLORS,
|
|
51
|
+
DEFAULT_FISH,
|
|
52
|
+
FISH_CATALOG,
|
|
53
|
+
METAQUARIUM_PARAMS,
|
|
54
|
+
MIAMI_VICE_COLORS,
|
|
55
|
+
createMetaquarium,
|
|
56
|
+
demoTrack,
|
|
57
|
+
metaquarium,
|
|
58
|
+
metaquariumManifest,
|
|
59
|
+
paramSpaceWith,
|
|
60
|
+
qualityFor,
|
|
61
|
+
resolveIpfsUrl,
|
|
62
|
+
withDefaults
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/metaquarium.ts"],"sourcesContent":["import type { ControlTrack, SaverContext, SaverInstance, SaverPlugin } from '@idle-screens/core';\nimport { detectCapabilities, computeTier } from '@idle-screens/capabilities';\nimport {\n metaquariumManifest,\n paramSpaceWith,\n type MetaquariumOptions,\n} from './manifest';\n\nexport function createMetaquarium(opts: MetaquariumOptions = {}): SaverPlugin {\n const space = paramSpaceWith(opts.params);\n return {\n manifest: {\n ...metaquariumManifest,\n id: opts.id ?? metaquariumManifest.id,\n label: opts.label ?? metaquariumManifest.label,\n paramSpace: space,\n },\n async mount(ctx: SaverContext): Promise<SaverInstance> {\n const [{ mountTank }, caps] = await Promise.all([\n import('./tank'),\n detectCapabilities(),\n ]);\n return mountTank(ctx, space, computeTier(caps));\n },\n };\n}\n\nexport const metaquarium: SaverPlugin = createMetaquarium();\n\nexport const demoTrack: ControlTrack = {\n program: 'metaquarium',\n seed: 42,\n duration: 30_000,\n loop: true,\n deltas: [\n { t: 5_000, path: 'autoRotate', value: 3, dur: 4_000 },\n { t: 15_000, path: 'fogColor', value: '#04002c', dur: 5_000 },\n { t: 22_000, path: 'fogColor', value: '#030009', dur: 5_000 },\n { t: 25_000, path: 'autoRotate', value: 0, dur: 4_000 },\n ],\n};\n"],"mappings":";;;;;;;;;;;;;;;;AACA,SAAS,oBAAoB,mBAAmB;AAOzC,SAAS,kBAAkB,OAA2B,CAAC,GAAgB;AAC5E,QAAM,QAAQ,eAAe,KAAK,MAAM;AACxC,SAAO;AAAA,IACL,UAAU;AAAA,MACR,GAAG;AAAA,MACH,IAAI,KAAK,MAAM,oBAAoB;AAAA,MACnC,OAAO,KAAK,SAAS,oBAAoB;AAAA,MACzC,YAAY;AAAA,IACd;AAAA,IACA,MAAM,MAAM,KAA2C;AACrD,YAAM,CAAC,EAAE,UAAU,GAAG,IAAI,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC9C,OAAO,oBAAQ;AAAA,QACf,mBAAmB;AAAA,MACrB,CAAC;AACD,aAAO,UAAU,KAAK,OAAO,YAAY,IAAI,CAAC;AAAA,IAChD;AAAA,EACF;AACF;AAEO,IAAM,cAA2B,kBAAkB;AAEnD,IAAM,YAA0B;AAAA,EACrC,SAAS;AAAA,EACT,MAAM;AAAA,EACN,UAAU;AAAA,EACV,MAAM;AAAA,EACN,QAAQ;AAAA,IACN,EAAE,GAAG,KAAO,MAAM,cAAc,OAAO,GAAG,KAAK,IAAM;AAAA,IACrD,EAAE,GAAG,MAAQ,MAAM,YAAY,OAAO,WAAW,KAAK,IAAM;AAAA,IAC5D,EAAE,GAAG,MAAQ,MAAM,YAAY,OAAO,WAAW,KAAK,IAAM;AAAA,IAC5D,EAAE,GAAG,MAAQ,MAAM,cAAc,OAAO,GAAG,KAAK,IAAM;AAAA,EACxD;AACF;","names":[]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ParamValue, SaverManifest, ParamSpace } from '@idle-screens/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zero-dependency manifest module (type-only imports). The channel server
|
|
5
|
+
* imports `@idle-screens/saver-metaquarium/manifest` to validate published
|
|
6
|
+
* params against this paramSpace without pulling three.js into the Worker.
|
|
7
|
+
*/
|
|
8
|
+
declare const METAQUARIUM_PARAMS: {
|
|
9
|
+
/** Camera orbit angle, degrees. */
|
|
10
|
+
cameraAzimuth: {
|
|
11
|
+
type: "number";
|
|
12
|
+
default: number;
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
ease: "smooth";
|
|
16
|
+
};
|
|
17
|
+
/** Camera height angle, degrees above the waterline plane. */
|
|
18
|
+
cameraElevation: {
|
|
19
|
+
type: "number";
|
|
20
|
+
default: number;
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
ease: "smooth";
|
|
24
|
+
};
|
|
25
|
+
/** Camera distance from tank center, world units. */
|
|
26
|
+
cameraDistance: {
|
|
27
|
+
type: "number";
|
|
28
|
+
default: number;
|
|
29
|
+
min: number;
|
|
30
|
+
max: number;
|
|
31
|
+
ease: "smooth";
|
|
32
|
+
};
|
|
33
|
+
/** Continuous orbit speed, degrees/second. Zero by default: the tank is
|
|
34
|
+
* still, letting the fish movement carry the scene. Steer up for an orbit. */
|
|
35
|
+
autoRotate: {
|
|
36
|
+
type: "number";
|
|
37
|
+
default: number;
|
|
38
|
+
min: number;
|
|
39
|
+
max: number;
|
|
40
|
+
ease: "smooth";
|
|
41
|
+
};
|
|
42
|
+
/** Visible fish. Default 1 = hero mode: one fish center-stage in its own
|
|
43
|
+
* tank; the pool grows on demand when steered up (never shrinks). */
|
|
44
|
+
fishCount: {
|
|
45
|
+
type: "number";
|
|
46
|
+
default: number;
|
|
47
|
+
min: number;
|
|
48
|
+
max: number;
|
|
49
|
+
ease: "step";
|
|
50
|
+
};
|
|
51
|
+
/** Swim time-scale multiplier. */
|
|
52
|
+
swimSpeed: {
|
|
53
|
+
type: "number";
|
|
54
|
+
default: number;
|
|
55
|
+
min: number;
|
|
56
|
+
max: number;
|
|
57
|
+
ease: "smooth";
|
|
58
|
+
};
|
|
59
|
+
/** Water/atmosphere color (background + fog). */
|
|
60
|
+
fogColor: {
|
|
61
|
+
type: "color";
|
|
62
|
+
default: string;
|
|
63
|
+
ease: "smooth";
|
|
64
|
+
};
|
|
65
|
+
/** GLB model URL for the fish (supports ipfs:// scheme). Package default
|
|
66
|
+
* is the IPFS hero fish so npm / channel hosts work without playground
|
|
67
|
+
* static assets; the playground overrides this to a local GLB. */
|
|
68
|
+
fishUrl: {
|
|
69
|
+
type: "string";
|
|
70
|
+
default: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/** The original's Miami-Vice body palette (scss-variables.ts) — seeded fish
|
|
74
|
+
* coats draw from this. Zero-dep so the server/manifest lane can surface it. */
|
|
75
|
+
declare const MIAMI_VICE_COLORS: string[];
|
|
76
|
+
/** The original's emissive/glow palette (`GLOW-*` parts, bloom layer). */
|
|
77
|
+
declare const BLOOM_COLORS: string[];
|
|
78
|
+
declare const metaquariumManifest: SaverManifest;
|
|
79
|
+
/** Options for {@link createMetaquarium}-style variants: a distinct id/label and
|
|
80
|
+
* overridden param defaults (e.g. a farm-connected tank). */
|
|
81
|
+
interface MetaquariumOptions {
|
|
82
|
+
id?: string;
|
|
83
|
+
label?: string;
|
|
84
|
+
params?: Partial<Record<keyof typeof METAQUARIUM_PARAMS, ParamValue>>;
|
|
85
|
+
}
|
|
86
|
+
/** Clone a paramSpace with default overrides; unknown keys are ignored. */
|
|
87
|
+
declare function withDefaults(space: ParamSpace, overrides?: Record<string, ParamValue>): ParamSpace;
|
|
88
|
+
/** Clone the metaquarium paramSpace with per-variant default overrides. */
|
|
89
|
+
declare function paramSpaceWith(overrides: MetaquariumOptions['params']): ParamSpace;
|
|
90
|
+
|
|
91
|
+
export { BLOOM_COLORS, METAQUARIUM_PARAMS, MIAMI_VICE_COLORS, type MetaquariumOptions, metaquariumManifest, paramSpaceWith, withDefaults };
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLOOM_COLORS,
|
|
3
|
+
METAQUARIUM_PARAMS,
|
|
4
|
+
MIAMI_VICE_COLORS,
|
|
5
|
+
metaquariumManifest,
|
|
6
|
+
paramSpaceWith,
|
|
7
|
+
withDefaults
|
|
8
|
+
} from "./chunk-4G3BQRZ3.js";
|
|
9
|
+
export {
|
|
10
|
+
BLOOM_COLORS,
|
|
11
|
+
METAQUARIUM_PARAMS,
|
|
12
|
+
MIAMI_VICE_COLORS,
|
|
13
|
+
metaquariumManifest,
|
|
14
|
+
paramSpaceWith,
|
|
15
|
+
withDefaults
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compileSwimPlan,
|
|
3
|
+
distanceAt,
|
|
4
|
+
effectivePixelRatio,
|
|
5
|
+
probeSoftwareGL,
|
|
6
|
+
qualityFor,
|
|
7
|
+
resolveIpfsUrl,
|
|
8
|
+
swimPoseAtDistance
|
|
9
|
+
} from "./chunk-YWDLFCBS.js";
|
|
10
|
+
import {
|
|
11
|
+
BLOOM_COLORS,
|
|
12
|
+
METAQUARIUM_PARAMS,
|
|
13
|
+
MIAMI_VICE_COLORS,
|
|
14
|
+
withDefaults
|
|
15
|
+
} from "./chunk-4G3BQRZ3.js";
|
|
16
|
+
|
|
17
|
+
// src/tank.ts
|
|
18
|
+
import {
|
|
19
|
+
defaultParams,
|
|
20
|
+
sampleTrack
|
|
21
|
+
} from "@idle-screens/core";
|
|
22
|
+
import {
|
|
23
|
+
AnimationMixer,
|
|
24
|
+
Box3,
|
|
25
|
+
CircleGeometry,
|
|
26
|
+
Color as Color2,
|
|
27
|
+
ConeGeometry,
|
|
28
|
+
Fog,
|
|
29
|
+
Group,
|
|
30
|
+
HemisphereLight,
|
|
31
|
+
LinearToneMapping,
|
|
32
|
+
MathUtils,
|
|
33
|
+
Mesh,
|
|
34
|
+
MeshBasicMaterial as MeshBasicMaterial2,
|
|
35
|
+
PerspectiveCamera,
|
|
36
|
+
Scene,
|
|
37
|
+
SphereGeometry,
|
|
38
|
+
SRGBColorSpace,
|
|
39
|
+
Vector3,
|
|
40
|
+
WebGLRenderer
|
|
41
|
+
} from "three";
|
|
42
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
43
|
+
import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
44
|
+
|
|
45
|
+
// src/materials.ts
|
|
46
|
+
import {
|
|
47
|
+
Color,
|
|
48
|
+
MeshBasicMaterial
|
|
49
|
+
} from "three";
|
|
50
|
+
var BODY_COATS = MIAMI_VICE_COLORS.filter((c) => c !== "#1c1c1c");
|
|
51
|
+
function materialsOf(mesh) {
|
|
52
|
+
return Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
53
|
+
}
|
|
54
|
+
function isEyes(m) {
|
|
55
|
+
return m.name.startsWith("EYES-") || /eye/i.test(m.name);
|
|
56
|
+
}
|
|
57
|
+
function isGlow(m) {
|
|
58
|
+
return m.name.startsWith("GLOW-") || /glow/i.test(m.name);
|
|
59
|
+
}
|
|
60
|
+
function forceOpaque(root) {
|
|
61
|
+
root.traverse((node) => {
|
|
62
|
+
const mesh = node;
|
|
63
|
+
if (!mesh.isMesh || !mesh.material) return;
|
|
64
|
+
for (const m of materialsOf(mesh)) {
|
|
65
|
+
m.transparent = false;
|
|
66
|
+
m.opacity = 1;
|
|
67
|
+
m.depthWrite = true;
|
|
68
|
+
m.alphaTest = 0;
|
|
69
|
+
m.needsUpdate = true;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function eyeNoseSign(root, axis) {
|
|
74
|
+
let eyeSum = 0;
|
|
75
|
+
let eyeN = 0;
|
|
76
|
+
let bodySum = 0;
|
|
77
|
+
let bodyN = 0;
|
|
78
|
+
root.updateMatrixWorld(true);
|
|
79
|
+
root.traverse((node) => {
|
|
80
|
+
const mesh = node;
|
|
81
|
+
if (!mesh.isMesh || !mesh.material || !mesh.geometry) return;
|
|
82
|
+
mesh.geometry.computeBoundingBox();
|
|
83
|
+
const bb = mesh.geometry.boundingBox;
|
|
84
|
+
if (!bb) return;
|
|
85
|
+
const centerLocal = bb.min.clone().add(bb.max).multiplyScalar(0.5);
|
|
86
|
+
const center = mesh.localToWorld(centerLocal);
|
|
87
|
+
const v = axis === "x" ? center.x : center.z;
|
|
88
|
+
if (materialsOf(mesh).some(isEyes)) {
|
|
89
|
+
eyeSum += v;
|
|
90
|
+
eyeN++;
|
|
91
|
+
} else {
|
|
92
|
+
bodySum += v;
|
|
93
|
+
bodyN++;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (eyeN === 0 || bodyN === 0) return 0;
|
|
97
|
+
return Math.sign(eyeSum / eyeN - bodySum / bodyN);
|
|
98
|
+
}
|
|
99
|
+
function applyNpcMaterials(root, rng) {
|
|
100
|
+
root.traverse((node) => {
|
|
101
|
+
const mesh = node;
|
|
102
|
+
if (!mesh.isMesh || !mesh.material) return;
|
|
103
|
+
const replaced = materialsOf(mesh).map((m) => {
|
|
104
|
+
if (isEyes(m)) return m;
|
|
105
|
+
if (m.map) return m;
|
|
106
|
+
if (isGlow(m)) {
|
|
107
|
+
const glow = new MeshBasicMaterial({ color: 0 });
|
|
108
|
+
glow.color.set(rng.pick(BLOOM_COLORS));
|
|
109
|
+
glow.name = m.name;
|
|
110
|
+
return glow;
|
|
111
|
+
}
|
|
112
|
+
const body = new MeshBasicMaterial({ color: new Color(rng.pick(BODY_COATS)) });
|
|
113
|
+
body.name = m.name;
|
|
114
|
+
return body;
|
|
115
|
+
});
|
|
116
|
+
mesh.material = Array.isArray(mesh.material) ? replaced : replaced[0];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/tank.ts
|
|
121
|
+
var BOUNDS = { radius: 120, yMin: 15, yMax: 72 };
|
|
122
|
+
var FISH_LENGTH = 18;
|
|
123
|
+
var MAX_FISH = METAQUARIUM_PARAMS.fishCount.max ?? 24;
|
|
124
|
+
var GLB_CONCURRENCY = 3;
|
|
125
|
+
var TEMPLATE_CACHE_CAP = 8;
|
|
126
|
+
var TEMPLATE_CACHE = /* @__PURE__ */ new Map();
|
|
127
|
+
var TankInstance = class {
|
|
128
|
+
ctxSaver;
|
|
129
|
+
space;
|
|
130
|
+
canvas;
|
|
131
|
+
ownsCanvas;
|
|
132
|
+
renderer;
|
|
133
|
+
quality;
|
|
134
|
+
govScale = 1;
|
|
135
|
+
frameTimes = [];
|
|
136
|
+
lastFrameAt = 0;
|
|
137
|
+
lastGovCheck = 0;
|
|
138
|
+
scene = new Scene();
|
|
139
|
+
camera;
|
|
140
|
+
fogColor = new Color2();
|
|
141
|
+
fish = [];
|
|
142
|
+
disposed = false;
|
|
143
|
+
w;
|
|
144
|
+
h;
|
|
145
|
+
frameId = null;
|
|
146
|
+
paused = false;
|
|
147
|
+
startT = 0;
|
|
148
|
+
t = 0;
|
|
149
|
+
params;
|
|
150
|
+
track = null;
|
|
151
|
+
thumbnail;
|
|
152
|
+
activeFishUrl = "";
|
|
153
|
+
constructor(ctx, space, quality) {
|
|
154
|
+
this.ctxSaver = ctx;
|
|
155
|
+
this.space = space;
|
|
156
|
+
this.quality = quality;
|
|
157
|
+
this.thumbnail = ctx.dpr < 0.5;
|
|
158
|
+
this.params = defaultParams(space);
|
|
159
|
+
this.w = ctx.width;
|
|
160
|
+
this.h = ctx.height;
|
|
161
|
+
if (ctx.surface instanceof HTMLCanvasElement) {
|
|
162
|
+
this.canvas = ctx.surface;
|
|
163
|
+
this.ownsCanvas = false;
|
|
164
|
+
} else {
|
|
165
|
+
this.canvas = document.createElement("canvas");
|
|
166
|
+
this.canvas.style.cssText = "display:block;width:100%;height:100%";
|
|
167
|
+
ctx.host.appendChild(this.canvas);
|
|
168
|
+
this.ownsCanvas = true;
|
|
169
|
+
}
|
|
170
|
+
this.canvas.addEventListener("webglcontextlost", (e) => e.preventDefault());
|
|
171
|
+
if (probeSoftwareGL()) this.quality = qualityFor("minimal");
|
|
172
|
+
this.renderer = new WebGLRenderer({
|
|
173
|
+
canvas: this.canvas,
|
|
174
|
+
antialias: this.quality.antialias,
|
|
175
|
+
stencil: false,
|
|
176
|
+
powerPreference: "high-performance"
|
|
177
|
+
});
|
|
178
|
+
this.renderer.setPixelRatio(this.pr());
|
|
179
|
+
this.renderer.setSize(this.w, this.h, false);
|
|
180
|
+
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
181
|
+
this.renderer.toneMapping = LinearToneMapping;
|
|
182
|
+
this.renderer.toneMappingExposure = 1;
|
|
183
|
+
this.camera = new PerspectiveCamera(55, this.w / this.h, 1, 1200);
|
|
184
|
+
const fogHex = String(this.space.fogColor?.default ?? "#030009");
|
|
185
|
+
this.fogColor.set(fogHex);
|
|
186
|
+
this.scene.fog = new Fog(this.fogColor.getHex(), 60, 500);
|
|
187
|
+
this.scene.background = this.fogColor;
|
|
188
|
+
const floor = new Mesh(
|
|
189
|
+
new CircleGeometry(600, 32),
|
|
190
|
+
new MeshBasicMaterial2({ color: 662835 })
|
|
191
|
+
);
|
|
192
|
+
floor.rotation.x = -Math.PI / 2;
|
|
193
|
+
this.scene.add(floor);
|
|
194
|
+
this.scene.add(new HemisphereLight(16777215, 4482730, 1.5));
|
|
195
|
+
void this.populate();
|
|
196
|
+
this.paused = ctx.reducedMotion;
|
|
197
|
+
if (this.paused) this.renderStill();
|
|
198
|
+
else this.start();
|
|
199
|
+
}
|
|
200
|
+
// ---- fish sourcing ----
|
|
201
|
+
str(key) {
|
|
202
|
+
const v = this.params[key];
|
|
203
|
+
return typeof v === "string" ? v : String(this.space[key]?.default ?? "");
|
|
204
|
+
}
|
|
205
|
+
async populate() {
|
|
206
|
+
const poolCap = this.thumbnail ? Math.min(6, MAX_FISH) : MAX_FISH;
|
|
207
|
+
const pool = Math.min(poolCap, this.quality.fishCap);
|
|
208
|
+
const fishUrl = this.activeFishUrl || this.str("fishUrl");
|
|
209
|
+
this.activeFishUrl = fishUrl;
|
|
210
|
+
const jobs = Array.from({ length: pool }, (_, i) => i);
|
|
211
|
+
let next = 0;
|
|
212
|
+
const worker = async () => {
|
|
213
|
+
while (next < jobs.length && !this.disposed) {
|
|
214
|
+
const i = jobs[next++];
|
|
215
|
+
const tpl = await this.template(fishUrl);
|
|
216
|
+
if (this.disposed || this.activeFishUrl !== fishUrl) return;
|
|
217
|
+
this.spawn(tpl, i);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
await Promise.all(Array.from({ length: GLB_CONCURRENCY }, worker));
|
|
221
|
+
if (this.paused) this.renderStill();
|
|
222
|
+
}
|
|
223
|
+
swapFish(url) {
|
|
224
|
+
for (const f of this.fish) {
|
|
225
|
+
f.mixer?.stopAllAction();
|
|
226
|
+
this.scene.remove(f.group);
|
|
227
|
+
f.group.traverse((o) => {
|
|
228
|
+
const mesh = o;
|
|
229
|
+
if (mesh.geometry) mesh.geometry.dispose();
|
|
230
|
+
const mats = Array.isArray(mesh.material) ? mesh.material : mesh.material ? [mesh.material] : [];
|
|
231
|
+
for (const m of mats) m.dispose();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
this.fish = [];
|
|
235
|
+
this.activeFishUrl = url;
|
|
236
|
+
void this.populate();
|
|
237
|
+
}
|
|
238
|
+
template(url) {
|
|
239
|
+
const key = url;
|
|
240
|
+
let p = TEMPLATE_CACHE.get(key);
|
|
241
|
+
if (!p) {
|
|
242
|
+
p = (async () => {
|
|
243
|
+
try {
|
|
244
|
+
const res = await fetch(resolveIpfsUrl(url));
|
|
245
|
+
if (!res.ok) throw new Error(`fish glb ${res.status}`);
|
|
246
|
+
const buf = await res.arrayBuffer();
|
|
247
|
+
const gltf = await new GLTFLoader().parseAsync(buf, "");
|
|
248
|
+
const scene = gltf.scene;
|
|
249
|
+
forceOpaque(scene);
|
|
250
|
+
const size = new Box3().setFromObject(scene).getSize(new Vector3());
|
|
251
|
+
const longX = size.x > size.z;
|
|
252
|
+
const nose = eyeNoseSign(scene, longX ? "x" : "z") || 1;
|
|
253
|
+
const yaw = longX ? nose > 0 ? -Math.PI / 2 : Math.PI / 2 : nose > 0 ? 0 : Math.PI;
|
|
254
|
+
return {
|
|
255
|
+
scene,
|
|
256
|
+
clip: gltf.animations[0] ?? null,
|
|
257
|
+
norm: FISH_LENGTH / (Math.max(size.x, size.y, size.z) || 1),
|
|
258
|
+
yaw
|
|
259
|
+
};
|
|
260
|
+
} catch {
|
|
261
|
+
TEMPLATE_CACHE.delete(key);
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
})();
|
|
265
|
+
if (TEMPLATE_CACHE.size >= TEMPLATE_CACHE_CAP) {
|
|
266
|
+
const oldest = TEMPLATE_CACHE.keys().next().value;
|
|
267
|
+
if (oldest !== void 0) TEMPLATE_CACHE.delete(oldest);
|
|
268
|
+
}
|
|
269
|
+
TEMPLATE_CACHE.set(key, p);
|
|
270
|
+
}
|
|
271
|
+
return p;
|
|
272
|
+
}
|
|
273
|
+
spawn(tpl, index) {
|
|
274
|
+
const rng = this.ctxSaver.rng.fork(1813);
|
|
275
|
+
const plan = compileSwimPlan(rng.fork(index), BOUNDS);
|
|
276
|
+
const group = new Group();
|
|
277
|
+
let mixer = null;
|
|
278
|
+
let tail = null;
|
|
279
|
+
let bodyNode = null;
|
|
280
|
+
let clipDuration = 0;
|
|
281
|
+
if (tpl) {
|
|
282
|
+
const body = cloneSkinned(tpl.scene);
|
|
283
|
+
applyNpcMaterials(body, this.ctxSaver.rng.fork(49319 + index));
|
|
284
|
+
body.scale.setScalar(tpl.norm);
|
|
285
|
+
body.rotation.y = tpl.yaw;
|
|
286
|
+
group.add(body);
|
|
287
|
+
bodyNode = body;
|
|
288
|
+
if (tpl.clip) {
|
|
289
|
+
mixer = new AnimationMixer(body);
|
|
290
|
+
mixer.clipAction(tpl.clip).play();
|
|
291
|
+
clipDuration = tpl.clip.duration;
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
const coat = this.ctxSaver.rng.fork(49319 + index).pick(MIAMI_VICE_COLORS);
|
|
295
|
+
const mat = new MeshBasicMaterial2({ color: new Color2(coat) });
|
|
296
|
+
const body = new Mesh(new SphereGeometry(FISH_LENGTH / 2, 12, 8), mat);
|
|
297
|
+
body.scale.set(1, 0.55, 0.4);
|
|
298
|
+
const tailMesh = new Mesh(
|
|
299
|
+
new ConeGeometry(FISH_LENGTH * 0.22, FISH_LENGTH * 0.5, 8),
|
|
300
|
+
mat
|
|
301
|
+
);
|
|
302
|
+
tailMesh.rotation.z = Math.PI / 2;
|
|
303
|
+
tailMesh.position.x = -FISH_LENGTH * 0.62;
|
|
304
|
+
group.add(body, tailMesh);
|
|
305
|
+
tail = tailMesh;
|
|
306
|
+
}
|
|
307
|
+
const baseScale = plan.cruise > 10 ? 1.1 : 0.8 + index % 5 * 0.1;
|
|
308
|
+
group.scale.multiplyScalar(baseScale);
|
|
309
|
+
this.scene.add(group);
|
|
310
|
+
this.fish.push({
|
|
311
|
+
group,
|
|
312
|
+
plan,
|
|
313
|
+
body: bodyNode,
|
|
314
|
+
baseScale,
|
|
315
|
+
mixer,
|
|
316
|
+
clipDuration,
|
|
317
|
+
tail
|
|
318
|
+
});
|
|
319
|
+
this.ctxSaver.host.dataset.mqFish = String(this.fish.length);
|
|
320
|
+
}
|
|
321
|
+
// ---- params / state ----
|
|
322
|
+
applyParams(t) {
|
|
323
|
+
if (this.track) this.params = sampleTrack(this.space, this.track, t);
|
|
324
|
+
}
|
|
325
|
+
num(key) {
|
|
326
|
+
const v = this.params[key];
|
|
327
|
+
return typeof v === "number" ? v : Number(this.space[key]?.default ?? 0);
|
|
328
|
+
}
|
|
329
|
+
setState(t) {
|
|
330
|
+
const tSec = t / 1e3;
|
|
331
|
+
this.applyParams(t);
|
|
332
|
+
const speed = this.num("swimSpeed");
|
|
333
|
+
const url = this.str("fishUrl");
|
|
334
|
+
if (url && url !== this.activeFishUrl) this.swapFish(url);
|
|
335
|
+
const az = MathUtils.degToRad(
|
|
336
|
+
this.num("cameraAzimuth") + this.num("autoRotate") * tSec
|
|
337
|
+
);
|
|
338
|
+
const el = MathUtils.degToRad(this.num("cameraElevation"));
|
|
339
|
+
const dist = this.num("cameraDistance");
|
|
340
|
+
this.camera.position.set(
|
|
341
|
+
Math.cos(el) * Math.sin(az) * dist,
|
|
342
|
+
Math.max(10, 15 + Math.sin(el) * dist),
|
|
343
|
+
Math.cos(el) * Math.cos(az) * dist
|
|
344
|
+
);
|
|
345
|
+
this.camera.lookAt(0, 35, 0);
|
|
346
|
+
const fogHex = String(
|
|
347
|
+
this.params.fogColor ?? this.space.fogColor?.default ?? "#030009"
|
|
348
|
+
);
|
|
349
|
+
this.fogColor.set(fogHex);
|
|
350
|
+
this.scene.fog.color.copy(this.fogColor);
|
|
351
|
+
const visible = Math.min(
|
|
352
|
+
Math.round(this.num("fishCount")),
|
|
353
|
+
this.quality.fishCap
|
|
354
|
+
);
|
|
355
|
+
for (let i = 0; i < this.fish.length; i++) {
|
|
356
|
+
const f = this.fish[i];
|
|
357
|
+
f.group.visible = i < visible;
|
|
358
|
+
if (!f.group.visible) continue;
|
|
359
|
+
const d = distanceAt(f.plan, tSec, speed);
|
|
360
|
+
const pose = swimPoseAtDistance(f.plan, d);
|
|
361
|
+
f.group.position.set(pose.x, pose.y, pose.z);
|
|
362
|
+
f.group.lookAt(pose.x + pose.fx, pose.y + pose.fy, pose.z + pose.fz);
|
|
363
|
+
f.group.rotateZ(pose.roll);
|
|
364
|
+
const breathe = 1 + Math.sin(tSec * 2.1 + i) * 8e-3;
|
|
365
|
+
f.group.scale.setScalar(f.baseScale * breathe);
|
|
366
|
+
if (f.mixer && f.clipDuration > 0) {
|
|
367
|
+
f.mixer.setTime(
|
|
368
|
+
(d * 0.045 % f.clipDuration + f.clipDuration) % f.clipDuration
|
|
369
|
+
);
|
|
370
|
+
} else if (f.tail) {
|
|
371
|
+
f.tail.rotation.y = Math.sin(tSec * speed * 6 + i) * 0.5;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// ---- render ----
|
|
376
|
+
renderScene() {
|
|
377
|
+
if (this.renderer.getContext()?.isContextLost?.()) return;
|
|
378
|
+
this.renderer.render(this.scene, this.camera);
|
|
379
|
+
}
|
|
380
|
+
start() {
|
|
381
|
+
if (this.frameId !== null || typeof requestAnimationFrame === "undefined")
|
|
382
|
+
return;
|
|
383
|
+
this.startT = 0;
|
|
384
|
+
this.frameId = requestAnimationFrame((now) => this.loop(now));
|
|
385
|
+
}
|
|
386
|
+
stop() {
|
|
387
|
+
if (this.frameId !== null) {
|
|
388
|
+
cancelAnimationFrame(this.frameId);
|
|
389
|
+
this.frameId = null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
loop(now) {
|
|
393
|
+
this.frameId = requestAnimationFrame((n) => this.loop(n));
|
|
394
|
+
if (this.startT === 0) this.startT = now;
|
|
395
|
+
this.governFrame(now);
|
|
396
|
+
this.t = now - this.startT;
|
|
397
|
+
this.setState(this.t);
|
|
398
|
+
this.renderScene();
|
|
399
|
+
}
|
|
400
|
+
pr() {
|
|
401
|
+
return Math.max(
|
|
402
|
+
0.5,
|
|
403
|
+
effectivePixelRatio(this.w, this.h, this.ctxSaver.dpr, this.quality) * this.govScale
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
applyPr() {
|
|
407
|
+
const pr = this.pr();
|
|
408
|
+
this.renderer.setPixelRatio(pr);
|
|
409
|
+
this.renderer.setSize(this.w, this.h, false);
|
|
410
|
+
}
|
|
411
|
+
governFrame(now) {
|
|
412
|
+
if (this.lastFrameAt > 0) {
|
|
413
|
+
const dt = now - this.lastFrameAt;
|
|
414
|
+
if (dt < 250) this.frameTimes.push(dt);
|
|
415
|
+
}
|
|
416
|
+
this.lastFrameAt = now;
|
|
417
|
+
if (now - this.lastGovCheck < 2e3 || this.frameTimes.length < 30) return;
|
|
418
|
+
this.lastGovCheck = now;
|
|
419
|
+
const sorted = [...this.frameTimes].sort((a, b) => a - b);
|
|
420
|
+
const median = sorted[sorted.length >> 1];
|
|
421
|
+
this.frameTimes = [];
|
|
422
|
+
if (median > 21 && this.govScale > 0.56) {
|
|
423
|
+
this.govScale *= 0.8;
|
|
424
|
+
this.applyPr();
|
|
425
|
+
} else if (median < 14 && this.govScale < 1) {
|
|
426
|
+
this.govScale = Math.min(1, this.govScale * 1.15);
|
|
427
|
+
this.applyPr();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
renderStill() {
|
|
431
|
+
this.setState(this.t);
|
|
432
|
+
this.renderScene();
|
|
433
|
+
}
|
|
434
|
+
// ---- SaverInstance ----
|
|
435
|
+
setPaused(paused) {
|
|
436
|
+
if (this.paused === paused) return;
|
|
437
|
+
this.paused = paused;
|
|
438
|
+
if (paused) {
|
|
439
|
+
this.stop();
|
|
440
|
+
this.renderStill();
|
|
441
|
+
} else {
|
|
442
|
+
this.start();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
resize(width, height, dpr) {
|
|
446
|
+
this.w = width;
|
|
447
|
+
this.h = height;
|
|
448
|
+
if (dpr !== void 0) this.ctxSaver.dpr = dpr;
|
|
449
|
+
this.applyPr();
|
|
450
|
+
this.camera.aspect = width / height;
|
|
451
|
+
this.camera.updateProjectionMatrix();
|
|
452
|
+
if (this.paused) this.renderStill();
|
|
453
|
+
}
|
|
454
|
+
applyTrack(track) {
|
|
455
|
+
this.track = track;
|
|
456
|
+
if (this.paused) this.renderStill();
|
|
457
|
+
}
|
|
458
|
+
renderFrame(t, _seed) {
|
|
459
|
+
this.t = t;
|
|
460
|
+
this.setState(t);
|
|
461
|
+
this.renderScene();
|
|
462
|
+
}
|
|
463
|
+
composition() {
|
|
464
|
+
return [
|
|
465
|
+
{
|
|
466
|
+
id: "tank",
|
|
467
|
+
label: "Tank",
|
|
468
|
+
kind: "surface",
|
|
469
|
+
el: this.canvas,
|
|
470
|
+
description: "WebGL2 aquarium: floor, fog, fish on Catmull-Rom spline paths"
|
|
471
|
+
}
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
dispose() {
|
|
475
|
+
this.disposed = true;
|
|
476
|
+
this.stop();
|
|
477
|
+
for (const f of this.fish) {
|
|
478
|
+
f.mixer?.stopAllAction();
|
|
479
|
+
this.scene.remove(f.group);
|
|
480
|
+
}
|
|
481
|
+
this.fish = [];
|
|
482
|
+
this.scene.traverse((o) => {
|
|
483
|
+
const mesh = o;
|
|
484
|
+
if (mesh.geometry) mesh.geometry.dispose();
|
|
485
|
+
const mats = Array.isArray(mesh.material) ? mesh.material : mesh.material ? [mesh.material] : [];
|
|
486
|
+
for (const m of mats) {
|
|
487
|
+
const tex = m.map;
|
|
488
|
+
if (tex) tex.dispose();
|
|
489
|
+
m.dispose();
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
this.renderer.dispose();
|
|
493
|
+
this.renderer.forceContextLoss();
|
|
494
|
+
if (this.ownsCanvas) this.canvas.remove();
|
|
495
|
+
delete this.ctxSaver.host.dataset.mqFish;
|
|
496
|
+
delete this.ctxSaver.host.dataset.mqBackend;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
function mountTank(ctx, space = METAQUARIUM_PARAMS, tier = "standard") {
|
|
500
|
+
const resolved = withDefaults(space, ctx.params);
|
|
501
|
+
const inst = new TankInstance(ctx, resolved, qualityFor(tier));
|
|
502
|
+
ctx.host.dataset.mqBackend = "webgl2";
|
|
503
|
+
return inst;
|
|
504
|
+
}
|
|
505
|
+
export {
|
|
506
|
+
mountTank
|
|
507
|
+
};
|
|
508
|
+
//# sourceMappingURL=tank-CQSFZYVF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tank.ts","../src/materials.ts"],"sourcesContent":["import type { CapabilityTier } from '@idle-screens/capabilities';\nimport {\n defaultParams,\n sampleTrack,\n type ControlTrack,\n type ParamSpace,\n type ParamValue,\n type SaverContext,\n type SaverInstance,\n type SaverLayer,\n} from '@idle-screens/core';\nimport {\n AnimationMixer,\n Box3,\n CircleGeometry,\n Color,\n ConeGeometry,\n Fog,\n Group,\n HemisphereLight,\n LinearToneMapping,\n MathUtils,\n Mesh,\n MeshBasicMaterial,\n PerspectiveCamera,\n Scene,\n SphereGeometry,\n SRGBColorSpace,\n Vector3,\n WebGLRenderer,\n type AnimationClip,\n type Material,\n type Object3D,\n} from 'three';\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';\nimport { clone as cloneSkinned } from 'three/examples/jsm/utils/SkeletonUtils.js';\nimport { resolveIpfsUrl } from './ipfs';\nimport { METAQUARIUM_PARAMS, withDefaults } from './manifest';\nimport {\n applyNpcMaterials,\n eyeNoseSign,\n forceOpaque,\n MIAMI_VICE_COLORS,\n} from './materials';\nimport {\n compileSwimPlan,\n distanceAt,\n swimPoseAtDistance,\n type SwimPlan,\n type TankBounds,\n} from './plan';\nimport {\n effectivePixelRatio,\n probeSoftwareGL,\n qualityFor,\n type TankQuality,\n} from './quality';\n\nconst BOUNDS: TankBounds = { radius: 120, yMin: 15, yMax: 72 };\nconst FISH_LENGTH = 18;\nconst MAX_FISH = METAQUARIUM_PARAMS.fishCount.max ?? 24;\nconst GLB_CONCURRENCY = 3;\n\n// ---------------------------------------------------------------------------\n// Template cache — shared across instances, never disposed by them.\n// ---------------------------------------------------------------------------\n\ninterface FishTemplate {\n scene: Object3D;\n clip: AnimationClip | null;\n norm: number;\n yaw: number;\n}\n\nconst TEMPLATE_CACHE_CAP = 8;\nconst TEMPLATE_CACHE = new Map<string, Promise<FishTemplate | null>>();\n\n// ---------------------------------------------------------------------------\n// Fish instance — one per visible fish in the scene.\n// ---------------------------------------------------------------------------\n\ninterface Fish {\n group: Group;\n plan: SwimPlan;\n body: Object3D | null;\n baseScale: number;\n mixer: AnimationMixer | null;\n clipDuration: number;\n tail: Object3D | null;\n}\n\n// ---------------------------------------------------------------------------\n// TankInstance — the studs: renderer, fog, floor, fish on spline plans.\n// ---------------------------------------------------------------------------\n\nclass TankInstance implements SaverInstance {\n private readonly ctxSaver: SaverContext;\n private readonly space: ParamSpace;\n private readonly canvas: HTMLCanvasElement;\n private readonly ownsCanvas: boolean;\n private readonly renderer: WebGLRenderer;\n private quality: TankQuality;\n private govScale = 1;\n private frameTimes: number[] = [];\n private lastFrameAt = 0;\n private lastGovCheck = 0;\n\n private readonly scene = new Scene();\n private readonly camera: PerspectiveCamera;\n private readonly fogColor = new Color();\n private fish: Fish[] = [];\n private disposed = false;\n\n private w: number;\n private h: number;\n private frameId: number | null = null;\n private paused = false;\n private startT = 0;\n private t = 0;\n\n private params: Record<string, ParamValue>;\n private track: ControlTrack | null = null;\n private readonly thumbnail: boolean;\n private activeFishUrl = '';\n\n constructor(ctx: SaverContext, space: ParamSpace, quality: TankQuality) {\n this.ctxSaver = ctx;\n this.space = space;\n this.quality = quality;\n this.thumbnail = ctx.dpr < 0.5;\n this.params = defaultParams(space);\n this.w = ctx.width;\n this.h = ctx.height;\n\n if (ctx.surface instanceof HTMLCanvasElement) {\n this.canvas = ctx.surface;\n this.ownsCanvas = false;\n } else {\n this.canvas = document.createElement('canvas');\n this.canvas.style.cssText = 'display:block;width:100%;height:100%';\n ctx.host.appendChild(this.canvas);\n this.ownsCanvas = true;\n }\n this.canvas.addEventListener('webglcontextlost', (e) => e.preventDefault());\n\n if (probeSoftwareGL()) this.quality = qualityFor('minimal');\n\n this.renderer = new WebGLRenderer({\n canvas: this.canvas,\n antialias: this.quality.antialias,\n stencil: false,\n powerPreference: 'high-performance',\n });\n this.renderer.setPixelRatio(this.pr());\n this.renderer.setSize(this.w, this.h, false);\n this.renderer.outputColorSpace = SRGBColorSpace;\n this.renderer.toneMapping = LinearToneMapping;\n this.renderer.toneMappingExposure = 1.0;\n\n this.camera = new PerspectiveCamera(55, this.w / this.h, 1, 1200);\n\n const fogHex = String(this.space.fogColor?.default ?? '#030009');\n this.fogColor.set(fogHex);\n this.scene.fog = new Fog(this.fogColor.getHex(), 60, 500);\n this.scene.background = this.fogColor;\n\n const floor = new Mesh(\n new CircleGeometry(600, 32),\n new MeshBasicMaterial({ color: 0x0a1d33 }),\n );\n floor.rotation.x = -Math.PI / 2;\n this.scene.add(floor);\n\n // Gentle hemisphere so MeshStandardMaterial eyes render their authored detail.\n // MeshBasicMaterial body/glow coats ignore it — zero visual cost for them.\n this.scene.add(new HemisphereLight(0xffffff, 0x4466aa, 1.5));\n\n void this.populate();\n\n this.paused = ctx.reducedMotion;\n if (this.paused) this.renderStill();\n else this.start();\n }\n\n // ---- fish sourcing ----\n\n private str(key: string): string {\n const v = this.params[key];\n return typeof v === 'string' ? v : String(this.space[key]?.default ?? '');\n }\n\n private async populate(): Promise<void> {\n const poolCap = this.thumbnail ? Math.min(6, MAX_FISH) : MAX_FISH;\n const pool = Math.min(poolCap, this.quality.fishCap);\n const fishUrl = this.activeFishUrl || this.str('fishUrl');\n this.activeFishUrl = fishUrl;\n\n const jobs = Array.from({ length: pool }, (_, i) => i);\n let next = 0;\n const worker = async (): Promise<void> => {\n while (next < jobs.length && !this.disposed) {\n const i = jobs[next++]!;\n const tpl = await this.template(fishUrl);\n if (this.disposed || this.activeFishUrl !== fishUrl) return;\n this.spawn(tpl, i);\n }\n };\n await Promise.all(Array.from({ length: GLB_CONCURRENCY }, worker));\n if (this.paused) this.renderStill();\n }\n\n private swapFish(url: string): void {\n for (const f of this.fish) {\n f.mixer?.stopAllAction();\n this.scene.remove(f.group);\n f.group.traverse((o) => {\n const mesh = o as Partial<Mesh>;\n if (mesh.geometry) mesh.geometry.dispose();\n const mats: Material[] = Array.isArray(mesh.material)\n ? mesh.material\n : mesh.material\n ? [mesh.material]\n : [];\n for (const m of mats) m.dispose();\n });\n }\n this.fish = [];\n this.activeFishUrl = url;\n void this.populate();\n }\n\n private template(url: string): Promise<FishTemplate | null> {\n const key = url;\n let p = TEMPLATE_CACHE.get(key);\n if (!p) {\n p = (async (): Promise<FishTemplate | null> => {\n try {\n const res = await fetch(resolveIpfsUrl(url));\n if (!res.ok) throw new Error(`fish glb ${res.status}`);\n const buf = await res.arrayBuffer();\n const gltf = await new GLTFLoader().parseAsync(buf, '');\n const scene = gltf.scene;\n forceOpaque(scene);\n const size = new Box3().setFromObject(scene).getSize(new Vector3());\n const longX = size.x > size.z;\n const nose = eyeNoseSign(scene, longX ? 'x' : 'z') || 1;\n const yaw = longX\n ? nose > 0\n ? -Math.PI / 2\n : Math.PI / 2\n : nose > 0\n ? 0\n : Math.PI;\n return {\n scene,\n clip: gltf.animations[0] ?? null,\n norm: FISH_LENGTH / (Math.max(size.x, size.y, size.z) || 1),\n yaw,\n };\n } catch {\n TEMPLATE_CACHE.delete(key);\n return null;\n }\n })();\n if (TEMPLATE_CACHE.size >= TEMPLATE_CACHE_CAP) {\n const oldest = TEMPLATE_CACHE.keys().next().value;\n if (oldest !== undefined) TEMPLATE_CACHE.delete(oldest);\n }\n TEMPLATE_CACHE.set(key, p);\n }\n return p;\n }\n\n private spawn(tpl: FishTemplate | null, index: number): void {\n const rng = this.ctxSaver.rng.fork(0x715);\n const plan = compileSwimPlan(rng.fork(index), BOUNDS);\n const group = new Group();\n let mixer: AnimationMixer | null = null;\n let tail: Object3D | null = null;\n let bodyNode: Object3D | null = null;\n let clipDuration = 0;\n\n if (tpl) {\n const body = cloneSkinned(tpl.scene);\n applyNpcMaterials(body, this.ctxSaver.rng.fork(0xc0a7 + index));\n body.scale.setScalar(tpl.norm);\n body.rotation.y = tpl.yaw;\n group.add(body);\n bodyNode = body;\n if (tpl.clip) {\n mixer = new AnimationMixer(body);\n mixer.clipAction(tpl.clip).play();\n clipDuration = tpl.clip.duration;\n }\n } else {\n const coat = this.ctxSaver.rng\n .fork(0xc0a7 + index)\n .pick(MIAMI_VICE_COLORS);\n const mat = new MeshBasicMaterial({ color: new Color(coat) });\n const body = new Mesh(new SphereGeometry(FISH_LENGTH / 2, 12, 8), mat);\n body.scale.set(1, 0.55, 0.4);\n const tailMesh = new Mesh(\n new ConeGeometry(FISH_LENGTH * 0.22, FISH_LENGTH * 0.5, 8),\n mat,\n );\n tailMesh.rotation.z = Math.PI / 2;\n tailMesh.position.x = -FISH_LENGTH * 0.62;\n group.add(body, tailMesh);\n tail = tailMesh;\n }\n\n const baseScale = plan.cruise > 10 ? 1.1 : 0.8 + (index % 5) * 0.1;\n group.scale.multiplyScalar(baseScale);\n this.scene.add(group);\n this.fish.push({\n group,\n plan,\n body: bodyNode,\n baseScale,\n mixer,\n clipDuration,\n tail,\n });\n this.ctxSaver.host.dataset.mqFish = String(this.fish.length);\n }\n\n // ---- params / state ----\n\n private applyParams(t: number): void {\n if (this.track) this.params = sampleTrack(this.space, this.track, t);\n }\n\n private num(key: string): number {\n const v = this.params[key];\n return typeof v === 'number' ? v : Number(this.space[key]?.default ?? 0);\n }\n\n private setState(t: number): void {\n const tSec = t / 1000;\n this.applyParams(t);\n const speed = this.num('swimSpeed');\n\n const url = this.str('fishUrl');\n if (url && url !== this.activeFishUrl) this.swapFish(url);\n\n // Camera orbit\n const az = MathUtils.degToRad(\n this.num('cameraAzimuth') + this.num('autoRotate') * tSec,\n );\n const el = MathUtils.degToRad(this.num('cameraElevation'));\n const dist = this.num('cameraDistance');\n this.camera.position.set(\n Math.cos(el) * Math.sin(az) * dist,\n Math.max(10, 15 + Math.sin(el) * dist),\n Math.cos(el) * Math.cos(az) * dist,\n );\n this.camera.lookAt(0, 35, 0);\n\n // Fog color\n const fogHex = String(\n this.params.fogColor ?? this.space.fogColor?.default ?? '#030009',\n );\n this.fogColor.set(fogHex);\n (this.scene.fog as Fog).color.copy(this.fogColor);\n\n // Fish\n const visible = Math.min(\n Math.round(this.num('fishCount')),\n this.quality.fishCap,\n );\n for (let i = 0; i < this.fish.length; i++) {\n const f = this.fish[i]!;\n f.group.visible = i < visible;\n if (!f.group.visible) continue;\n\n const d = distanceAt(f.plan, tSec, speed);\n const pose = swimPoseAtDistance(f.plan, d);\n f.group.position.set(pose.x, pose.y, pose.z);\n f.group.lookAt(pose.x + pose.fx, pose.y + pose.fy, pose.z + pose.fz);\n f.group.rotateZ(pose.roll);\n\n const breathe = 1 + Math.sin(tSec * 2.1 + i) * 0.008;\n f.group.scale.setScalar(f.baseScale * breathe);\n\n if (f.mixer && f.clipDuration > 0) {\n f.mixer.setTime(\n (((d * 0.045) % f.clipDuration) + f.clipDuration) % f.clipDuration,\n );\n } else if (f.tail) {\n f.tail.rotation.y = Math.sin(tSec * speed * 6 + i) * 0.5;\n }\n }\n }\n\n // ---- render ----\n\n private renderScene(): void {\n if (this.renderer.getContext()?.isContextLost?.()) return;\n this.renderer.render(this.scene, this.camera);\n }\n\n private start(): void {\n if (this.frameId !== null || typeof requestAnimationFrame === 'undefined')\n return;\n this.startT = 0;\n this.frameId = requestAnimationFrame((now) => this.loop(now));\n }\n\n private stop(): void {\n if (this.frameId !== null) {\n cancelAnimationFrame(this.frameId);\n this.frameId = null;\n }\n }\n\n private loop(now: number): void {\n this.frameId = requestAnimationFrame((n) => this.loop(n));\n if (this.startT === 0) this.startT = now;\n this.governFrame(now);\n this.t = now - this.startT;\n this.setState(this.t);\n this.renderScene();\n }\n\n private pr(): number {\n return Math.max(\n 0.5,\n effectivePixelRatio(this.w, this.h, this.ctxSaver.dpr, this.quality) *\n this.govScale,\n );\n }\n\n private applyPr(): void {\n const pr = this.pr();\n this.renderer.setPixelRatio(pr);\n this.renderer.setSize(this.w, this.h, false);\n }\n\n private governFrame(now: number): void {\n if (this.lastFrameAt > 0) {\n const dt = now - this.lastFrameAt;\n if (dt < 250) this.frameTimes.push(dt);\n }\n this.lastFrameAt = now;\n if (now - this.lastGovCheck < 2000 || this.frameTimes.length < 30) return;\n this.lastGovCheck = now;\n const sorted = [...this.frameTimes].sort((a, b) => a - b);\n const median = sorted[sorted.length >> 1]!;\n this.frameTimes = [];\n if (median > 21 && this.govScale > 0.56) {\n this.govScale *= 0.8;\n this.applyPr();\n } else if (median < 14 && this.govScale < 1) {\n this.govScale = Math.min(1, this.govScale * 1.15);\n this.applyPr();\n }\n }\n\n private renderStill(): void {\n this.setState(this.t);\n this.renderScene();\n }\n\n // ---- SaverInstance ----\n\n setPaused(paused: boolean): void {\n if (this.paused === paused) return;\n this.paused = paused;\n if (paused) {\n this.stop();\n this.renderStill();\n } else {\n this.start();\n }\n }\n\n resize(width: number, height: number, dpr?: number): void {\n this.w = width;\n this.h = height;\n if (dpr !== undefined) this.ctxSaver.dpr = dpr;\n this.applyPr();\n this.camera.aspect = width / height;\n this.camera.updateProjectionMatrix();\n if (this.paused) this.renderStill();\n }\n\n applyTrack(track: ControlTrack): void {\n this.track = track;\n if (this.paused) this.renderStill();\n }\n\n renderFrame(t: number, _seed: number): void {\n this.t = t;\n this.setState(t);\n this.renderScene();\n }\n\n composition(): SaverLayer[] {\n return [\n {\n id: 'tank',\n label: 'Tank',\n kind: 'surface',\n el: this.canvas,\n description:\n 'WebGL2 aquarium: floor, fog, fish on Catmull-Rom spline paths',\n },\n ];\n }\n\n dispose(): void {\n this.disposed = true;\n this.stop();\n for (const f of this.fish) {\n f.mixer?.stopAllAction();\n this.scene.remove(f.group);\n }\n this.fish = [];\n this.scene.traverse((o) => {\n const mesh = o as Partial<Mesh>;\n if (mesh.geometry) mesh.geometry.dispose();\n const mats: Material[] = Array.isArray(mesh.material)\n ? mesh.material\n : mesh.material\n ? [mesh.material]\n : [];\n for (const m of mats) {\n const tex = (m as Partial<MeshBasicMaterial>).map;\n if (tex) tex.dispose();\n m.dispose();\n }\n });\n this.renderer.dispose();\n this.renderer.forceContextLoss();\n if (this.ownsCanvas) this.canvas.remove();\n delete this.ctxSaver.host.dataset.mqFish;\n delete this.ctxSaver.host.dataset.mqBackend;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory — single entry point for mounting a tank.\n// ---------------------------------------------------------------------------\n\nexport function mountTank(\n ctx: SaverContext,\n space: ParamSpace = METAQUARIUM_PARAMS,\n tier: CapabilityTier = 'standard',\n): SaverInstance {\n const resolved = withDefaults(space, ctx.params);\n const inst = new TankInstance(ctx, resolved, qualityFor(tier));\n ctx.host.dataset.mqBackend = 'webgl2';\n return inst;\n}\n","import type { Rng } from '@idle-screens/core';\nimport {\n Color,\n MeshBasicMaterial,\n type Material,\n type Mesh,\n type Object3D,\n} from 'three';\nimport { MIAMI_VICE_COLORS, BLOOM_COLORS } from './manifest';\n\nexport { MIAMI_VICE_COLORS, BLOOM_COLORS };\n\nconst BODY_COATS = MIAMI_VICE_COLORS.filter((c) => c !== '#1c1c1c');\n\nfunction materialsOf(mesh: Mesh): Material[] {\n return Array.isArray(mesh.material) ? mesh.material : [mesh.material];\n}\n\nfunction isEyes(m: Material): boolean {\n return m.name.startsWith('EYES-') || /eye/i.test(m.name);\n}\n\nfunction isGlow(m: Material): boolean {\n return m.name.startsWith('GLOW-') || /glow/i.test(m.name);\n}\n\nexport function forceOpaque(root: Object3D): void {\n root.traverse((node) => {\n const mesh = node as Mesh;\n if (!mesh.isMesh || !mesh.material) return;\n for (const m of materialsOf(mesh)) {\n m.transparent = false;\n m.opacity = 1;\n m.depthWrite = true;\n m.alphaTest = 0;\n m.needsUpdate = true;\n }\n });\n}\n\nexport function eyeNoseSign(root: Object3D, axis: 'x' | 'z'): number {\n let eyeSum = 0;\n let eyeN = 0;\n let bodySum = 0;\n let bodyN = 0;\n root.updateMatrixWorld(true);\n root.traverse((node) => {\n const mesh = node as Mesh;\n if (!mesh.isMesh || !mesh.material || !mesh.geometry) return;\n mesh.geometry.computeBoundingBox();\n const bb = mesh.geometry.boundingBox;\n if (!bb) return;\n const centerLocal = bb.min.clone().add(bb.max).multiplyScalar(0.5);\n const center = mesh.localToWorld(centerLocal);\n const v = axis === 'x' ? center.x : center.z;\n if (materialsOf(mesh).some(isEyes)) {\n eyeSum += v;\n eyeN++;\n } else {\n bodySum += v;\n bodyN++;\n }\n });\n if (eyeN === 0 || bodyN === 0) return 0;\n return Math.sign(eyeSum / eyeN - bodySum / bodyN);\n}\n\n/**\n * Seeded palette coat: body → unlit palette color, GLOW-* → colored emissive\n * MeshBasicMaterial, EYES-* → untouched, textured materials → untouched\n * (their atlas IS the look). All unlit — no scene lights needed.\n */\nexport function applyNpcMaterials(root: Object3D, rng: Rng): void {\n root.traverse((node) => {\n const mesh = node as Mesh;\n if (!mesh.isMesh || !mesh.material) return;\n const replaced = materialsOf(mesh).map((m) => {\n if (isEyes(m)) return m;\n if ((m as Partial<MeshBasicMaterial>).map) return m;\n if (isGlow(m)) {\n const glow = new MeshBasicMaterial({ color: 0x000000 });\n glow.color.set(rng.pick(BLOOM_COLORS));\n glow.name = m.name;\n return glow;\n }\n const body = new MeshBasicMaterial({ color: new Color(rng.pick(BODY_COATS)) });\n body.name = m.name;\n return body;\n });\n mesh.material = Array.isArray(mesh.material) ? replaced : replaced[0]!;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AACA;AAAA,EACE;AAAA,EACA;AAAA,OAOK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,qBAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,kBAAkB;AAC3B,SAAS,SAAS,oBAAoB;;;AClCtC;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAKP,IAAM,aAAa,kBAAkB,OAAO,CAAC,MAAM,MAAM,SAAS;AAElE,SAAS,YAAY,MAAwB;AAC3C,SAAO,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ;AACtE;AAEA,SAAS,OAAO,GAAsB;AACpC,SAAO,EAAE,KAAK,WAAW,OAAO,KAAK,OAAO,KAAK,EAAE,IAAI;AACzD;AAEA,SAAS,OAAO,GAAsB;AACpC,SAAO,EAAE,KAAK,WAAW,OAAO,KAAK,QAAQ,KAAK,EAAE,IAAI;AAC1D;AAEO,SAAS,YAAY,MAAsB;AAChD,OAAK,SAAS,CAAC,SAAS;AACtB,UAAM,OAAO;AACb,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,SAAU;AACpC,eAAW,KAAK,YAAY,IAAI,GAAG;AACjC,QAAE,cAAc;AAChB,QAAE,UAAU;AACZ,QAAE,aAAa;AACf,QAAE,YAAY;AACd,QAAE,cAAc;AAAA,IAClB;AAAA,EACF,CAAC;AACH;AAEO,SAAS,YAAY,MAAgB,MAAyB;AACnE,MAAI,SAAS;AACb,MAAI,OAAO;AACX,MAAI,UAAU;AACd,MAAI,QAAQ;AACZ,OAAK,kBAAkB,IAAI;AAC3B,OAAK,SAAS,CAAC,SAAS;AACtB,UAAM,OAAO;AACb,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,YAAY,CAAC,KAAK,SAAU;AACtD,SAAK,SAAS,mBAAmB;AACjC,UAAM,KAAK,KAAK,SAAS;AACzB,QAAI,CAAC,GAAI;AACT,UAAM,cAAc,GAAG,IAAI,MAAM,EAAE,IAAI,GAAG,GAAG,EAAE,eAAe,GAAG;AACjE,UAAM,SAAS,KAAK,aAAa,WAAW;AAC5C,UAAM,IAAI,SAAS,MAAM,OAAO,IAAI,OAAO;AAC3C,QAAI,YAAY,IAAI,EAAE,KAAK,MAAM,GAAG;AAClC,gBAAU;AACV;AAAA,IACF,OAAO;AACL,iBAAW;AACX;AAAA,IACF;AAAA,EACF,CAAC;AACD,MAAI,SAAS,KAAK,UAAU,EAAG,QAAO;AACtC,SAAO,KAAK,KAAK,SAAS,OAAO,UAAU,KAAK;AAClD;AAOO,SAAS,kBAAkB,MAAgB,KAAgB;AAChE,OAAK,SAAS,CAAC,SAAS;AACtB,UAAM,OAAO;AACb,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,SAAU;AACpC,UAAM,WAAW,YAAY,IAAI,EAAE,IAAI,CAAC,MAAM;AAC5C,UAAI,OAAO,CAAC,EAAG,QAAO;AACtB,UAAK,EAAiC,IAAK,QAAO;AAClD,UAAI,OAAO,CAAC,GAAG;AACb,cAAM,OAAO,IAAI,kBAAkB,EAAE,OAAO,EAAS,CAAC;AACtD,aAAK,MAAM,IAAI,IAAI,KAAK,YAAY,CAAC;AACrC,aAAK,OAAO,EAAE;AACd,eAAO;AAAA,MACT;AACA,YAAM,OAAO,IAAI,kBAAkB,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,UAAU,CAAC,EAAE,CAAC;AAC7E,WAAK,OAAO,EAAE;AACd,aAAO;AAAA,IACT,CAAC;AACD,SAAK,WAAW,MAAM,QAAQ,KAAK,QAAQ,IAAI,WAAW,SAAS,CAAC;AAAA,EACtE,CAAC;AACH;;;ADjCA,IAAM,SAAqB,EAAE,QAAQ,KAAK,MAAM,IAAI,MAAM,GAAG;AAC7D,IAAM,cAAc;AACpB,IAAM,WAAW,mBAAmB,UAAU,OAAO;AACrD,IAAM,kBAAkB;AAaxB,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB,oBAAI,IAA0C;AAoBrE,IAAM,eAAN,MAA4C;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT;AAAA,EACA,WAAW;AAAA,EACX,aAAuB,CAAC;AAAA,EACxB,cAAc;AAAA,EACd,eAAe;AAAA,EAEN,QAAQ,IAAI,MAAM;AAAA,EAClB;AAAA,EACA,WAAW,IAAIC,OAAM;AAAA,EAC9B,OAAe,CAAC;AAAA,EAChB,WAAW;AAAA,EAEX;AAAA,EACA;AAAA,EACA,UAAyB;AAAA,EACzB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,IAAI;AAAA,EAEJ;AAAA,EACA,QAA6B;AAAA,EACpB;AAAA,EACT,gBAAgB;AAAA,EAExB,YAAY,KAAmB,OAAmB,SAAsB;AACtE,SAAK,WAAW;AAChB,SAAK,QAAQ;AACb,SAAK,UAAU;AACf,SAAK,YAAY,IAAI,MAAM;AAC3B,SAAK,SAAS,cAAc,KAAK;AACjC,SAAK,IAAI,IAAI;AACb,SAAK,IAAI,IAAI;AAEb,QAAI,IAAI,mBAAmB,mBAAmB;AAC5C,WAAK,SAAS,IAAI;AAClB,WAAK,aAAa;AAAA,IACpB,OAAO;AACL,WAAK,SAAS,SAAS,cAAc,QAAQ;AAC7C,WAAK,OAAO,MAAM,UAAU;AAC5B,UAAI,KAAK,YAAY,KAAK,MAAM;AAChC,WAAK,aAAa;AAAA,IACpB;AACA,SAAK,OAAO,iBAAiB,oBAAoB,CAAC,MAAM,EAAE,eAAe,CAAC;AAE1E,QAAI,gBAAgB,EAAG,MAAK,UAAU,WAAW,SAAS;AAE1D,SAAK,WAAW,IAAI,cAAc;AAAA,MAChC,QAAQ,KAAK;AAAA,MACb,WAAW,KAAK,QAAQ;AAAA,MACxB,SAAS;AAAA,MACT,iBAAiB;AAAA,IACnB,CAAC;AACD,SAAK,SAAS,cAAc,KAAK,GAAG,CAAC;AACrC,SAAK,SAAS,QAAQ,KAAK,GAAG,KAAK,GAAG,KAAK;AAC3C,SAAK,SAAS,mBAAmB;AACjC,SAAK,SAAS,cAAc;AAC5B,SAAK,SAAS,sBAAsB;AAEpC,SAAK,SAAS,IAAI,kBAAkB,IAAI,KAAK,IAAI,KAAK,GAAG,GAAG,IAAI;AAEhE,UAAM,SAAS,OAAO,KAAK,MAAM,UAAU,WAAW,SAAS;AAC/D,SAAK,SAAS,IAAI,MAAM;AACxB,SAAK,MAAM,MAAM,IAAI,IAAI,KAAK,SAAS,OAAO,GAAG,IAAI,GAAG;AACxD,SAAK,MAAM,aAAa,KAAK;AAE7B,UAAM,QAAQ,IAAI;AAAA,MAChB,IAAI,eAAe,KAAK,EAAE;AAAA,MAC1B,IAAIC,mBAAkB,EAAE,OAAO,OAAS,CAAC;AAAA,IAC3C;AACA,UAAM,SAAS,IAAI,CAAC,KAAK,KAAK;AAC9B,SAAK,MAAM,IAAI,KAAK;AAIpB,SAAK,MAAM,IAAI,IAAI,gBAAgB,UAAU,SAAU,GAAG,CAAC;AAE3D,SAAK,KAAK,SAAS;AAEnB,SAAK,SAAS,IAAI;AAClB,QAAI,KAAK,OAAQ,MAAK,YAAY;AAAA,QAC7B,MAAK,MAAM;AAAA,EAClB;AAAA;AAAA,EAIQ,IAAI,KAAqB;AAC/B,UAAM,IAAI,KAAK,OAAO,GAAG;AACzB,WAAO,OAAO,MAAM,WAAW,IAAI,OAAO,KAAK,MAAM,GAAG,GAAG,WAAW,EAAE;AAAA,EAC1E;AAAA,EAEA,MAAc,WAA0B;AACtC,UAAM,UAAU,KAAK,YAAY,KAAK,IAAI,GAAG,QAAQ,IAAI;AACzD,UAAM,OAAO,KAAK,IAAI,SAAS,KAAK,QAAQ,OAAO;AACnD,UAAM,UAAU,KAAK,iBAAiB,KAAK,IAAI,SAAS;AACxD,SAAK,gBAAgB;AAErB,UAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC;AACrD,QAAI,OAAO;AACX,UAAM,SAAS,YAA2B;AACxC,aAAO,OAAO,KAAK,UAAU,CAAC,KAAK,UAAU;AAC3C,cAAM,IAAI,KAAK,MAAM;AACrB,cAAM,MAAM,MAAM,KAAK,SAAS,OAAO;AACvC,YAAI,KAAK,YAAY,KAAK,kBAAkB,QAAS;AACrD,aAAK,MAAM,KAAK,CAAC;AAAA,MACnB;AAAA,IACF;AACA,UAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,gBAAgB,GAAG,MAAM,CAAC;AACjE,QAAI,KAAK,OAAQ,MAAK,YAAY;AAAA,EACpC;AAAA,EAEQ,SAAS,KAAmB;AAClC,eAAW,KAAK,KAAK,MAAM;AACzB,QAAE,OAAO,cAAc;AACvB,WAAK,MAAM,OAAO,EAAE,KAAK;AACzB,QAAE,MAAM,SAAS,CAAC,MAAM;AACtB,cAAM,OAAO;AACb,YAAI,KAAK,SAAU,MAAK,SAAS,QAAQ;AACzC,cAAM,OAAmB,MAAM,QAAQ,KAAK,QAAQ,IAChD,KAAK,WACL,KAAK,WACL,CAAC,KAAK,QAAQ,IACd,CAAC;AACL,mBAAW,KAAK,KAAM,GAAE,QAAQ;AAAA,MAClC,CAAC;AAAA,IACH;AACA,SAAK,OAAO,CAAC;AACb,SAAK,gBAAgB;AACrB,SAAK,KAAK,SAAS;AAAA,EACrB;AAAA,EAEQ,SAAS,KAA2C;AAC1D,UAAM,MAAM;AACZ,QAAI,IAAI,eAAe,IAAI,GAAG;AAC9B,QAAI,CAAC,GAAG;AACN,WAAK,YAA0C;AAC7C,YAAI;AACF,gBAAM,MAAM,MAAM,MAAM,eAAe,GAAG,CAAC;AAC3C,cAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,YAAY,IAAI,MAAM,EAAE;AACrD,gBAAM,MAAM,MAAM,IAAI,YAAY;AAClC,gBAAM,OAAO,MAAM,IAAI,WAAW,EAAE,WAAW,KAAK,EAAE;AACtD,gBAAM,QAAQ,KAAK;AACnB,sBAAY,KAAK;AACjB,gBAAM,OAAO,IAAI,KAAK,EAAE,cAAc,KAAK,EAAE,QAAQ,IAAI,QAAQ,CAAC;AAClE,gBAAM,QAAQ,KAAK,IAAI,KAAK;AAC5B,gBAAM,OAAO,YAAY,OAAO,QAAQ,MAAM,GAAG,KAAK;AACtD,gBAAM,MAAM,QACR,OAAO,IACL,CAAC,KAAK,KAAK,IACX,KAAK,KAAK,IACZ,OAAO,IACP,IACA,KAAK;AACT,iBAAO;AAAA,YACL;AAAA,YACA,MAAM,KAAK,WAAW,CAAC,KAAK;AAAA,YAC5B,MAAM,eAAe,KAAK,IAAI,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,KAAK;AAAA,YACzD;AAAA,UACF;AAAA,QACF,QAAQ;AACN,yBAAe,OAAO,GAAG;AACzB,iBAAO;AAAA,QACT;AAAA,MACF,GAAG;AACH,UAAI,eAAe,QAAQ,oBAAoB;AAC7C,cAAM,SAAS,eAAe,KAAK,EAAE,KAAK,EAAE;AAC5C,YAAI,WAAW,OAAW,gBAAe,OAAO,MAAM;AAAA,MACxD;AACA,qBAAe,IAAI,KAAK,CAAC;AAAA,IAC3B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,MAAM,KAA0B,OAAqB;AAC3D,UAAM,MAAM,KAAK,SAAS,IAAI,KAAK,IAAK;AACxC,UAAM,OAAO,gBAAgB,IAAI,KAAK,KAAK,GAAG,MAAM;AACpD,UAAM,QAAQ,IAAI,MAAM;AACxB,QAAI,QAA+B;AACnC,QAAI,OAAwB;AAC5B,QAAI,WAA4B;AAChC,QAAI,eAAe;AAEnB,QAAI,KAAK;AACP,YAAM,OAAO,aAAa,IAAI,KAAK;AACnC,wBAAkB,MAAM,KAAK,SAAS,IAAI,KAAK,QAAS,KAAK,CAAC;AAC9D,WAAK,MAAM,UAAU,IAAI,IAAI;AAC7B,WAAK,SAAS,IAAI,IAAI;AACtB,YAAM,IAAI,IAAI;AACd,iBAAW;AACX,UAAI,IAAI,MAAM;AACZ,gBAAQ,IAAI,eAAe,IAAI;AAC/B,cAAM,WAAW,IAAI,IAAI,EAAE,KAAK;AAChC,uBAAe,IAAI,KAAK;AAAA,MAC1B;AAAA,IACF,OAAO;AACL,YAAM,OAAO,KAAK,SAAS,IACxB,KAAK,QAAS,KAAK,EACnB,KAAK,iBAAiB;AACzB,YAAM,MAAM,IAAIA,mBAAkB,EAAE,OAAO,IAAID,OAAM,IAAI,EAAE,CAAC;AAC5D,YAAM,OAAO,IAAI,KAAK,IAAI,eAAe,cAAc,GAAG,IAAI,CAAC,GAAG,GAAG;AACrE,WAAK,MAAM,IAAI,GAAG,MAAM,GAAG;AAC3B,YAAM,WAAW,IAAI;AAAA,QACnB,IAAI,aAAa,cAAc,MAAM,cAAc,KAAK,CAAC;AAAA,QACzD;AAAA,MACF;AACA,eAAS,SAAS,IAAI,KAAK,KAAK;AAChC,eAAS,SAAS,IAAI,CAAC,cAAc;AACrC,YAAM,IAAI,MAAM,QAAQ;AACxB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,KAAK,SAAS,KAAK,MAAM,MAAO,QAAQ,IAAK;AAC/D,UAAM,MAAM,eAAe,SAAS;AACpC,SAAK,MAAM,IAAI,KAAK;AACpB,SAAK,KAAK,KAAK;AAAA,MACb;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,SAAK,SAAS,KAAK,QAAQ,SAAS,OAAO,KAAK,KAAK,MAAM;AAAA,EAC7D;AAAA;AAAA,EAIQ,YAAY,GAAiB;AACnC,QAAI,KAAK,MAAO,MAAK,SAAS,YAAY,KAAK,OAAO,KAAK,OAAO,CAAC;AAAA,EACrE;AAAA,EAEQ,IAAI,KAAqB;AAC/B,UAAM,IAAI,KAAK,OAAO,GAAG;AACzB,WAAO,OAAO,MAAM,WAAW,IAAI,OAAO,KAAK,MAAM,GAAG,GAAG,WAAW,CAAC;AAAA,EACzE;AAAA,EAEQ,SAAS,GAAiB;AAChC,UAAM,OAAO,IAAI;AACjB,SAAK,YAAY,CAAC;AAClB,UAAM,QAAQ,KAAK,IAAI,WAAW;AAElC,UAAM,MAAM,KAAK,IAAI,SAAS;AAC9B,QAAI,OAAO,QAAQ,KAAK,cAAe,MAAK,SAAS,GAAG;AAGxD,UAAM,KAAK,UAAU;AAAA,MACnB,KAAK,IAAI,eAAe,IAAI,KAAK,IAAI,YAAY,IAAI;AAAA,IACvD;AACA,UAAM,KAAK,UAAU,SAAS,KAAK,IAAI,iBAAiB,CAAC;AACzD,UAAM,OAAO,KAAK,IAAI,gBAAgB;AACtC,SAAK,OAAO,SAAS;AAAA,MACnB,KAAK,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI;AAAA,MAC9B,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,EAAE,IAAI,IAAI;AAAA,MACrC,KAAK,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI;AAAA,IAChC;AACA,SAAK,OAAO,OAAO,GAAG,IAAI,CAAC;AAG3B,UAAM,SAAS;AAAA,MACb,KAAK,OAAO,YAAY,KAAK,MAAM,UAAU,WAAW;AAAA,IAC1D;AACA,SAAK,SAAS,IAAI,MAAM;AACxB,IAAC,KAAK,MAAM,IAAY,MAAM,KAAK,KAAK,QAAQ;AAGhD,UAAM,UAAU,KAAK;AAAA,MACnB,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC;AAAA,MAChC,KAAK,QAAQ;AAAA,IACf;AACA,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK,QAAQ,KAAK;AACzC,YAAM,IAAI,KAAK,KAAK,CAAC;AACrB,QAAE,MAAM,UAAU,IAAI;AACtB,UAAI,CAAC,EAAE,MAAM,QAAS;AAEtB,YAAM,IAAI,WAAW,EAAE,MAAM,MAAM,KAAK;AACxC,YAAM,OAAO,mBAAmB,EAAE,MAAM,CAAC;AACzC,QAAE,MAAM,SAAS,IAAI,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAC3C,QAAE,MAAM,OAAO,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,EAAE;AACnE,QAAE,MAAM,QAAQ,KAAK,IAAI;AAEzB,YAAM,UAAU,IAAI,KAAK,IAAI,OAAO,MAAM,CAAC,IAAI;AAC/C,QAAE,MAAM,MAAM,UAAU,EAAE,YAAY,OAAO;AAE7C,UAAI,EAAE,SAAS,EAAE,eAAe,GAAG;AACjC,UAAE,MAAM;AAAA,WACH,IAAI,QAAS,EAAE,eAAgB,EAAE,gBAAgB,EAAE;AAAA,QACxD;AAAA,MACF,WAAW,EAAE,MAAM;AACjB,UAAE,KAAK,SAAS,IAAI,KAAK,IAAI,OAAO,QAAQ,IAAI,CAAC,IAAI;AAAA,MACvD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAIQ,cAAoB;AAC1B,QAAI,KAAK,SAAS,WAAW,GAAG,gBAAgB,EAAG;AACnD,SAAK,SAAS,OAAO,KAAK,OAAO,KAAK,MAAM;AAAA,EAC9C;AAAA,EAEQ,QAAc;AACpB,QAAI,KAAK,YAAY,QAAQ,OAAO,0BAA0B;AAC5D;AACF,SAAK,SAAS;AACd,SAAK,UAAU,sBAAsB,CAAC,QAAQ,KAAK,KAAK,GAAG,CAAC;AAAA,EAC9D;AAAA,EAEQ,OAAa;AACnB,QAAI,KAAK,YAAY,MAAM;AACzB,2BAAqB,KAAK,OAAO;AACjC,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAEQ,KAAK,KAAmB;AAC9B,SAAK,UAAU,sBAAsB,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC;AACxD,QAAI,KAAK,WAAW,EAAG,MAAK,SAAS;AACrC,SAAK,YAAY,GAAG;AACpB,SAAK,IAAI,MAAM,KAAK;AACpB,SAAK,SAAS,KAAK,CAAC;AACpB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,KAAa;AACnB,WAAO,KAAK;AAAA,MACV;AAAA,MACA,oBAAoB,KAAK,GAAG,KAAK,GAAG,KAAK,SAAS,KAAK,KAAK,OAAO,IACjE,KAAK;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,UAAgB;AACtB,UAAM,KAAK,KAAK,GAAG;AACnB,SAAK,SAAS,cAAc,EAAE;AAC9B,SAAK,SAAS,QAAQ,KAAK,GAAG,KAAK,GAAG,KAAK;AAAA,EAC7C;AAAA,EAEQ,YAAY,KAAmB;AACrC,QAAI,KAAK,cAAc,GAAG;AACxB,YAAM,KAAK,MAAM,KAAK;AACtB,UAAI,KAAK,IAAK,MAAK,WAAW,KAAK,EAAE;AAAA,IACvC;AACA,SAAK,cAAc;AACnB,QAAI,MAAM,KAAK,eAAe,OAAQ,KAAK,WAAW,SAAS,GAAI;AACnE,SAAK,eAAe;AACpB,UAAM,SAAS,CAAC,GAAG,KAAK,UAAU,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AACxD,UAAM,SAAS,OAAO,OAAO,UAAU,CAAC;AACxC,SAAK,aAAa,CAAC;AACnB,QAAI,SAAS,MAAM,KAAK,WAAW,MAAM;AACvC,WAAK,YAAY;AACjB,WAAK,QAAQ;AAAA,IACf,WAAW,SAAS,MAAM,KAAK,WAAW,GAAG;AAC3C,WAAK,WAAW,KAAK,IAAI,GAAG,KAAK,WAAW,IAAI;AAChD,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA,EAEQ,cAAoB;AAC1B,SAAK,SAAS,KAAK,CAAC;AACpB,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA,EAIA,UAAU,QAAuB;AAC/B,QAAI,KAAK,WAAW,OAAQ;AAC5B,SAAK,SAAS;AACd,QAAI,QAAQ;AACV,WAAK,KAAK;AACV,WAAK,YAAY;AAAA,IACnB,OAAO;AACL,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAEA,OAAO,OAAe,QAAgB,KAAoB;AACxD,SAAK,IAAI;AACT,SAAK,IAAI;AACT,QAAI,QAAQ,OAAW,MAAK,SAAS,MAAM;AAC3C,SAAK,QAAQ;AACb,SAAK,OAAO,SAAS,QAAQ;AAC7B,SAAK,OAAO,uBAAuB;AACnC,QAAI,KAAK,OAAQ,MAAK,YAAY;AAAA,EACpC;AAAA,EAEA,WAAW,OAA2B;AACpC,SAAK,QAAQ;AACb,QAAI,KAAK,OAAQ,MAAK,YAAY;AAAA,EACpC;AAAA,EAEA,YAAY,GAAW,OAAqB;AAC1C,SAAK,IAAI;AACT,SAAK,SAAS,CAAC;AACf,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,cAA4B;AAC1B,WAAO;AAAA,MACL;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,IAAI,KAAK;AAAA,QACT,aACE;AAAA,MACJ;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,SAAK,WAAW;AAChB,SAAK,KAAK;AACV,eAAW,KAAK,KAAK,MAAM;AACzB,QAAE,OAAO,cAAc;AACvB,WAAK,MAAM,OAAO,EAAE,KAAK;AAAA,IAC3B;AACA,SAAK,OAAO,CAAC;AACb,SAAK,MAAM,SAAS,CAAC,MAAM;AACzB,YAAM,OAAO;AACb,UAAI,KAAK,SAAU,MAAK,SAAS,QAAQ;AACzC,YAAM,OAAmB,MAAM,QAAQ,KAAK,QAAQ,IAChD,KAAK,WACL,KAAK,WACL,CAAC,KAAK,QAAQ,IACd,CAAC;AACL,iBAAW,KAAK,MAAM;AACpB,cAAM,MAAO,EAAiC;AAC9C,YAAI,IAAK,KAAI,QAAQ;AACrB,UAAE,QAAQ;AAAA,MACZ;AAAA,IACF,CAAC;AACD,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,iBAAiB;AAC/B,QAAI,KAAK,WAAY,MAAK,OAAO,OAAO;AACxC,WAAO,KAAK,SAAS,KAAK,QAAQ;AAClC,WAAO,KAAK,SAAS,KAAK,QAAQ;AAAA,EACpC;AACF;AAMO,SAAS,UACd,KACA,QAAoB,oBACpB,OAAuB,YACR;AACf,QAAM,WAAW,aAAa,OAAO,IAAI,MAAM;AAC/C,QAAM,OAAO,IAAI,aAAa,KAAK,UAAU,WAAW,IAAI,CAAC;AAC7D,MAAI,KAAK,QAAQ,YAAY;AAC7B,SAAO;AACT;","names":["Color","MeshBasicMaterial","Color","MeshBasicMaterial"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idle-screens/saver-metaquarium",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Metaquarium screensaver for idle-screens: a three.js fish tank with seeded analytic swim, GLB fish streamed from the Metaquarium farm, selective bloom, and a steerable camera. The screen is a tank.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Shawn Partridge",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/shaaaaawn/idle-screens",
|
|
10
|
+
"directory": "packages/saver-metaquarium"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/shaaaaawn/idle-screens#readme",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./manifest": {
|
|
24
|
+
"types": "./dist/manifest.d.ts",
|
|
25
|
+
"import": "./dist/manifest.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"screensaver",
|
|
33
|
+
"idle",
|
|
34
|
+
"ambient",
|
|
35
|
+
"aquarium",
|
|
36
|
+
"metaquarium",
|
|
37
|
+
"three",
|
|
38
|
+
"webgl",
|
|
39
|
+
"fish"
|
|
40
|
+
],
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/shaaaaawn/idle-screens/issues"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"three": "^0.185.0",
|
|
49
|
+
"@idle-screens/capabilities": "^0.2.1"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@idle-screens/core": "0.4.4"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/three": "^0.185.0",
|
|
56
|
+
"@idle-screens/core": "0.4.4"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsup",
|
|
60
|
+
"dev": "tsup --watch",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"lint": "eslint src"
|
|
63
|
+
}
|
|
64
|
+
}
|