@buoy-gg/shared-ui 7.0.26 → 7.0.28
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/lib/commonjs/JsModal.js +156 -20
- package/lib/commonjs/JsModal.web.js +85 -39
- package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +14 -4
- package/lib/commonjs/dataViewer/diffThemes.js +20 -18
- package/lib/commonjs/dataViewer/tree/TreeDiffViewer.js +8 -8
- package/lib/commonjs/hooks/safe-area-impl.js +1 -1
- package/lib/commonjs/icons/lucide-icons.js +74 -15
- package/lib/commonjs/index.js +86 -0
- package/lib/commonjs/ui/backgrounds/BackgroundSwitcher.js +248 -74
- package/lib/commonjs/ui/backgrounds/README.md +102 -20
- package/lib/commonjs/ui/backgrounds/backgroundStore.js +20 -6
- package/lib/commonjs/ui/backgrounds/index.js +91 -1
- package/lib/commonjs/ui/backgrounds/primitives.js +50 -14
- package/lib/commonjs/ui/backgrounds/registry.js +41 -64
- package/lib/commonjs/ui/backgrounds/rng.js +21 -0
- package/lib/commonjs/ui/backgrounds/skyClock.js +45 -0
- package/lib/commonjs/ui/backgrounds/variants/daylight.js +453 -0
- package/lib/commonjs/ui/backgrounds/variants/deepField.js +60 -30
- package/lib/commonjs/ui/backgrounds/variants/hyperspace.js +218 -0
- package/lib/commonjs/ui/backgrounds/variants/liveSky.js +898 -0
- package/lib/commonjs/ui/backgrounds/variants/meteors.js +257 -0
- package/lib/commonjs/ui/backgrounds/variants/moonlight.js +472 -0
- package/lib/commonjs/ui/backgrounds/variants/underwater.js +757 -0
- package/lib/commonjs/ui/components/Badge.js +11 -10
- package/lib/commonjs/ui/components/CompactRow.js +35 -18
- package/lib/commonjs/ui/components/CopyButton.js +6 -4
- package/lib/commonjs/ui/components/DevToolsCard.js +16 -9
- package/lib/commonjs/ui/components/DynamicFilterView.js +43 -41
- package/lib/commonjs/ui/components/EventStepperFooter.js +38 -10
- package/lib/commonjs/ui/components/FilterComponents.js +11 -11
- package/lib/commonjs/ui/components/ModalHeader.js +28 -15
- package/lib/commonjs/ui/components/SectionHeader.js +3 -1
- package/lib/commonjs/ui/components/StorageTypeBadge.js +3 -2
- package/lib/commonjs/ui/components/TabSelector.js +56 -38
- package/lib/commonjs/ui/components/ValueTypeBadge.js +7 -6
- package/lib/commonjs/ui/components/WindowControls.js +37 -10
- package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +8 -0
- package/lib/commonjs/ui/night/nightTheme.js +120 -0
- package/lib/commonjs/ui/night/primitives.js +498 -0
- package/lib/module/JsModal.js +156 -20
- package/lib/module/JsModal.web.js +83 -39
- package/lib/module/dataViewer/VirtualizedDataExplorer.js +14 -4
- package/lib/module/dataViewer/diffThemes.js +20 -18
- package/lib/module/dataViewer/tree/TreeDiffViewer.js +9 -9
- package/lib/module/hooks/safe-area-impl.js +1 -1
- package/lib/module/icons/lucide-icons.js +74 -15
- package/lib/module/index.js +4 -0
- package/lib/module/ui/backgrounds/BackgroundSwitcher.js +250 -76
- package/lib/module/ui/backgrounds/README.md +102 -20
- package/lib/module/ui/backgrounds/backgroundStore.js +19 -5
- package/lib/module/ui/backgrounds/index.js +11 -2
- package/lib/module/ui/backgrounds/primitives.js +53 -16
- package/lib/module/ui/backgrounds/registry.js +41 -64
- package/lib/module/ui/backgrounds/rng.js +20 -0
- package/lib/module/ui/backgrounds/skyClock.js +39 -0
- package/lib/module/ui/backgrounds/variants/daylight.js +449 -0
- package/lib/module/ui/backgrounds/variants/deepField.js +62 -31
- package/lib/module/ui/backgrounds/variants/hyperspace.js +214 -0
- package/lib/module/ui/backgrounds/variants/liveSky.js +893 -0
- package/lib/module/ui/backgrounds/variants/meteors.js +253 -0
- package/lib/module/ui/backgrounds/variants/moonlight.js +468 -0
- package/lib/module/ui/backgrounds/variants/underwater.js +753 -0
- package/lib/module/ui/components/Badge.js +11 -10
- package/lib/module/ui/components/CompactRow.js +35 -18
- package/lib/module/ui/components/CopyButton.js +6 -4
- package/lib/module/ui/components/DevToolsCard.js +16 -9
- package/lib/module/ui/components/DynamicFilterView.js +43 -41
- package/lib/module/ui/components/EventStepperFooter.js +38 -10
- package/lib/module/ui/components/FilterComponents.js +11 -11
- package/lib/module/ui/components/ModalHeader.js +28 -15
- package/lib/module/ui/components/SectionHeader.js +3 -1
- package/lib/module/ui/components/StorageTypeBadge.js +3 -2
- package/lib/module/ui/components/TabSelector.js +57 -39
- package/lib/module/ui/components/ValueTypeBadge.js +7 -6
- package/lib/module/ui/components/WindowControls.js +37 -10
- package/lib/module/ui/gameUI/constants/gameUIColors.js +8 -0
- package/lib/module/ui/night/nightTheme.js +116 -0
- package/lib/module/ui/night/primitives.js +485 -0
- package/lib/typescript/commonjs/JsModal.d.ts +18 -0
- package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/JsModal.web.d.ts +13 -0
- package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/diffThemes.d.ts +3 -1
- package/lib/typescript/commonjs/dataViewer/diffThemes.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/commonjs/icons/lucide-icons.d.ts +12 -1
- package/lib/typescript/commonjs/icons/lucide-icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts +26 -6
- package/lib/typescript/commonjs/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts +3 -0
- package/lib/typescript/commonjs/ui/backgrounds/backgroundStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/index.d.ts +7 -1
- package/lib/typescript/commonjs/ui/backgrounds/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts +18 -3
- package/lib/typescript/commonjs/ui/backgrounds/primitives.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts +8 -0
- package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts +6 -0
- package/lib/typescript/commonjs/ui/backgrounds/rng.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/skyClock.d.ts +17 -0
- package/lib/typescript/commonjs/ui/backgrounds/skyClock.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/backgrounds/types.d.ts +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/variants/daylight.d.ts +30 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/daylight.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts +19 -4
- package/lib/typescript/commonjs/ui/backgrounds/variants/deepField.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/backgrounds/variants/hyperspace.d.ts +39 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/hyperspace.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/liveSky.d.ts +41 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/liveSky.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts +26 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/meteors.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/moonlight.d.ts +35 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/moonlight.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/underwater.d.ts +51 -0
- package/lib/typescript/commonjs/ui/backgrounds/variants/underwater.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/Badge.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +3 -1
- package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/CopyButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts +3 -1
- package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/DynamicFilterView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts +2 -0
- package/lib/typescript/commonjs/ui/components/EventStepperFooter.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/StorageTypeBadge.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/TabSelector.d.ts +9 -0
- package/lib/typescript/commonjs/ui/components/TabSelector.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ValueTypeBadge.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/WindowControls.d.ts +4 -2
- package/lib/typescript/commonjs/ui/components/WindowControls.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +8 -0
- package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/night/nightTheme.d.ts +113 -0
- package/lib/typescript/commonjs/ui/night/nightTheme.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/night/primitives.d.ts +111 -0
- package/lib/typescript/commonjs/ui/night/primitives.d.ts.map +1 -0
- package/lib/typescript/module/JsModal.d.ts +18 -0
- package/lib/typescript/module/JsModal.d.ts.map +1 -1
- package/lib/typescript/module/JsModal.web.d.ts +13 -0
- package/lib/typescript/module/JsModal.web.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/diffThemes.d.ts +3 -1
- package/lib/typescript/module/dataViewer/diffThemes.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
- package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/module/icons/lucide-icons.d.ts +12 -1
- package/lib/typescript/module/icons/lucide-icons.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts +26 -6
- package/lib/typescript/module/ui/backgrounds/BackgroundSwitcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts +3 -0
- package/lib/typescript/module/ui/backgrounds/backgroundStore.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/index.d.ts +7 -1
- package/lib/typescript/module/ui/backgrounds/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/primitives.d.ts +18 -3
- package/lib/typescript/module/ui/backgrounds/primitives.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/registry.d.ts +8 -0
- package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/rng.d.ts +6 -0
- package/lib/typescript/module/ui/backgrounds/rng.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/skyClock.d.ts +17 -0
- package/lib/typescript/module/ui/backgrounds/skyClock.d.ts.map +1 -0
- package/lib/typescript/module/ui/backgrounds/types.d.ts +1 -1
- package/lib/typescript/module/ui/backgrounds/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/variants/daylight.d.ts +30 -0
- package/lib/typescript/module/ui/backgrounds/variants/daylight.d.ts.map +1 -0
- package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts +19 -4
- package/lib/typescript/module/ui/backgrounds/variants/deepField.d.ts.map +1 -1
- package/lib/typescript/module/ui/backgrounds/variants/hyperspace.d.ts +39 -0
- package/lib/typescript/module/ui/backgrounds/variants/hyperspace.d.ts.map +1 -0
- package/lib/typescript/module/ui/backgrounds/variants/liveSky.d.ts +41 -0
- package/lib/typescript/module/ui/backgrounds/variants/liveSky.d.ts.map +1 -0
- package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts +26 -0
- package/lib/typescript/module/ui/backgrounds/variants/meteors.d.ts.map +1 -0
- package/lib/typescript/module/ui/backgrounds/variants/moonlight.d.ts +35 -0
- package/lib/typescript/module/ui/backgrounds/variants/moonlight.d.ts.map +1 -0
- package/lib/typescript/module/ui/backgrounds/variants/underwater.d.ts +51 -0
- package/lib/typescript/module/ui/backgrounds/variants/underwater.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/Badge.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/CompactRow.d.ts +3 -1
- package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/CopyButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/DevToolsCard.d.ts +3 -1
- package/lib/typescript/module/ui/components/DevToolsCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/DynamicFilterView.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/EventStepperFooter.d.ts +2 -0
- package/lib/typescript/module/ui/components/EventStepperFooter.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/StorageTypeBadge.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/TabSelector.d.ts +9 -0
- package/lib/typescript/module/ui/components/TabSelector.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ValueTypeBadge.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/WindowControls.d.ts +4 -2
- package/lib/typescript/module/ui/components/WindowControls.d.ts.map +1 -1
- package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +8 -0
- package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
- package/lib/typescript/module/ui/night/nightTheme.d.ts +113 -0
- package/lib/typescript/module/ui/night/nightTheme.d.ts.map +1 -0
- package/lib/typescript/module/ui/night/primitives.d.ts +111 -0
- package/lib/typescript/module/ui/night/primitives.d.ts.map +1 -0
- package/package.json +4 -4
- package/lib/commonjs/ui/backgrounds/variants/lattice.js +0 -579
- package/lib/commonjs/ui/backgrounds/variants/starfield.js +0 -210
- package/lib/module/ui/backgrounds/variants/lattice.js +0 -575
- package/lib/module/ui/backgrounds/variants/starfield.js +0 -206
- package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts +0 -33
- package/lib/typescript/commonjs/ui/backgrounds/variants/lattice.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts +0 -12
- package/lib/typescript/commonjs/ui/backgrounds/variants/starfield.d.ts.map +0 -1
- package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts +0 -33
- package/lib/typescript/module/ui/backgrounds/variants/lattice.d.ts.map +0 -1
- package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts +0 -12
- package/lib/typescript/module/ui/backgrounds/variants/starfield.d.ts.map +0 -1
|
@@ -11,27 +11,65 @@ top.
|
|
|
11
11
|
</View>
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The preset is a single shared, persisted choice (`backgroundStore
|
|
15
|
-
switching it in one tool switches it everywhere.
|
|
16
|
-
|
|
14
|
+
The preset is a single shared, persisted choice (`backgroundStore`, default:
|
|
15
|
+
**Hyperspace**), so switching it in one tool switches it everywhere. Shipping
|
|
16
|
+
surfaces get it automatically: `JsModal` and `BottomSheet` mount
|
|
17
|
+
`<ToolBackground />` inside their content box by default (opt out per tool
|
|
18
|
+
with `background={false}`; the network tool does, because it mounts its own so
|
|
19
|
+
the desktop inline panel matches).
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
`<BackgroundSwitcher />` is the ‹ prev / next › review strip used to audition
|
|
22
|
+
the catalogue (plus Live Sky's time slider and ▶ movie). It is currently
|
|
23
|
+
mounted NOWHERE — the catalogue picked its winner — but stays in the package:
|
|
24
|
+
drop it back at the top of a tool body to audition changes.
|
|
25
|
+
|
|
26
|
+
## The catalogue
|
|
27
|
+
|
|
28
|
+
Round 3 cut the set to the keepers plus three underwater variations (round 4
|
|
29
|
+
folded the Meteors preset into Deep Field):
|
|
19
30
|
|
|
20
|
-
- **Starfield** (`variants/starfield.tsx`) — the cheap scenic preset: 99 stars
|
|
21
|
-
quantised into five depth bands.
|
|
22
31
|
- **Deep Field** (`variants/deepField.tsx`) — the Everlights
|
|
23
32
|
`ParallaxDriftBackground`, ported 1:1: same seed and generator call order,
|
|
24
33
|
same 160 stars, same far-plane bias, sizes, palette and drift. Speed is
|
|
25
34
|
quantised into 18 bands (a distinct speed costs a distinct animated node);
|
|
26
|
-
everything else stays per-star.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
everything else stays per-star. The priciest preset — the file documents
|
|
36
|
+
exactly what could and couldn't come across from Skia. The shipping preset
|
|
37
|
+
also composes the **meteor layer** (`variants/meteors.tsx`, once its own
|
|
38
|
+
"Meteors" preset): shooting stars that streak in, hit a WALL of the surface
|
|
39
|
+
and burst — flash, expanding shockwave ring (half-clipped by the edge, which
|
|
40
|
+
sells the impact), sparks ricocheting back into the room; each meteor's
|
|
41
|
+
whole life cycle is keyframed into interpolate stops on one plain loop.
|
|
42
|
+
`DeepFieldTunable` stays meteor-free for the bench.
|
|
43
|
+
- **Hyperspace** (`variants/hyperspace.tsx`) — the Everlights Skia
|
|
44
|
+
`HyperspaceBackground` (lightspeed starfield) re-derived for this contract:
|
|
45
|
+
the per-frame `screen = plane/z` projection becomes scale keyframes on
|
|
46
|
+
phase-offset shells (a layer scaled by 1/z moves every star on exactly the
|
|
47
|
+
original's hyperbolic curve), with the wrap snap hidden inside the
|
|
48
|
+
original's own 6% fade edges. One animated node total; raster OFF on the
|
|
49
|
+
shells because a cached bitmap scaled 8× goes to mush.
|
|
50
|
+
- **Live Sky** (`variants/liveSky.tsx`) — the cove on the real local clock:
|
|
51
|
+
sun up 06:30–19:00 on a sine arc, moon on the night shift, dawn/golden/
|
|
52
|
+
sunset/dusk as lerped palette keyframes (night = Moonlight's palette, noon =
|
|
53
|
+
Daylight's). ZERO extra animated nodes — the sun moves ~0.4px/minute, so the
|
|
54
|
+
scene is laid out statically per minute and a once-a-minute AppState-gated
|
|
55
|
+
tick re-renders it; this also sidesteps the native driver's
|
|
56
|
+
transform/opacity-only limit for the colour transitions. Discs draw UNDER
|
|
57
|
+
the opaque sea so the horizon clips a rising/setting body for free. While
|
|
58
|
+
it's showing, the switcher grows a time slider (writes `skyClock.ts`,
|
|
59
|
+
5-minute quantized, not persisted) to scrub the whole cycle by hand; LIVE
|
|
60
|
+
snaps back to the clock. See `LIVE_SKY_PLAN.md`.
|
|
61
|
+
- **The underwater family** (`variants/underwater.tsx`) — Abyss, Bubbles,
|
|
62
|
+
Jellyfish. One register — lit from the surface, near-black at the bottom,
|
|
63
|
+
motion in tens of seconds — with different life in the water: marine snow,
|
|
64
|
+
rising bubbles (a ring plus an off-centre highlight is what makes a bubble),
|
|
65
|
+
and pulsing jellies that traverse bottom-to-top entirely off-screen at the
|
|
66
|
+
loop seam.
|
|
67
|
+
|
|
68
|
+
Catalogue trims (2026-08-20): Off, Night Drift, Moonlight, Daylight, Kelp and
|
|
69
|
+
Kelp Forest were cut from the registry. `variants/moonlight.tsx` and
|
|
70
|
+
`variants/daylight.tsx` stay in the tree — Live Sky's palettes are documented
|
|
71
|
+
against them and the bg-bench lanes (`MoonlightTunable`/`DaylightTunable`)
|
|
72
|
+
still drive them — they just aren't offered in the switcher.
|
|
35
73
|
|
|
36
74
|
## The constraint
|
|
37
75
|
|
|
@@ -58,6 +96,15 @@ translucent layers. For the same reason nothing here uses `transformOrigin`
|
|
|
58
96
|
|
|
59
97
|
Loops stop when the app backgrounds, and unmount with the tool.
|
|
60
98
|
|
|
99
|
+
4. **Geometry never scales with the modal.** Resizing the surface must crop
|
|
100
|
+
the scene, not stretch it — rays, plants, blooms and particles size
|
|
101
|
+
themselves from `Dimensions.get("window")` and only their POSITIONS
|
|
102
|
+
distribute across the measured width/height (the preset root clips with
|
|
103
|
+
`overflow: "hidden"`). Smooth gradients (base washes, vignettes) may track
|
|
104
|
+
the live size — their scaling is imperceptible. The one sanctioned
|
|
105
|
+
exception is geometry that is ABOUT the surface itself, like the meteors'
|
|
106
|
+
wall-impact points.
|
|
107
|
+
|
|
61
108
|
**Known trap:** `Animated.sequence([timing, Animated.loop(...)])` silently fails
|
|
62
109
|
to drive the value on the native driver — no warning, the field renders and
|
|
63
110
|
never moves. It's the obvious way to phase-offset siblings; don't. Stagger via
|
|
@@ -66,8 +113,9 @@ different periods, or bake the offset into the `interpolate`.
|
|
|
66
113
|
### Measured
|
|
67
114
|
|
|
68
115
|
Network tool open on an iPhone 16 simulator, via perf-monitor and
|
|
69
|
-
`measure_renders`. Starfield
|
|
70
|
-
|
|
116
|
+
`measure_renders`. (Starfield and the lattice presets were measured before the
|
|
117
|
+
round-3 cull; the rows stay as calibration points — Deep Field is still the
|
|
118
|
+
ceiling, and the newer presets all run 3–7 nodes.)
|
|
71
119
|
|
|
72
120
|
**Steady state** — 12–13s idle recordings:
|
|
73
121
|
|
|
@@ -87,6 +135,40 @@ The commits in every column are the network tool's own once-a-minute
|
|
|
87
135
|
reported one component render in every configuration — the animation contributes
|
|
88
136
|
zero React work while it runs, which is rule 1 holding regardless of node count.
|
|
89
137
|
|
|
138
|
+
**Moonlight v3** (buoy-optimize loop, iPhone 17 Pro sim, bg-bench, 3-run
|
|
139
|
+
medians, reproduced across two batches):
|
|
140
|
+
|
|
141
|
+
| | UI FPS | UI jank | JS FPS |
|
|
142
|
+
|---|---:|---:|---:|
|
|
143
|
+
| control (no background) | 60.0 | 0 | 58.3 |
|
|
144
|
+
| Moonlight, shipping (~330 views, 5 nodes) | 59.9 | 0 | 57.5–58.1 |
|
|
145
|
+
| glade ×12 (~1.2k blocks) | 59.5 | 0 | 57.0 |
|
|
146
|
+
| glade ×30 (~3.1k blocks) | 56.4–56.8 | 2 | 53.8 |
|
|
147
|
+
|
|
148
|
+
Shipping Moonlight is indistinguishable from no background; the UI cliff sits
|
|
149
|
+
between 12× and 30× the shipping glade. Structural variants (fused glade,
|
|
150
|
+
raster off, chrome off) all tied with shipping at 1× AND failed to separate at
|
|
151
|
+
×30 (±1 FPS, and raster-off ballooned memory 287→351MB), so the shipping
|
|
152
|
+
structure stays. Bench lane: `bg-bench?v=ml-*` drives `MoonlightTunable`.
|
|
153
|
+
|
|
154
|
+
**Confirmed on a real iPhone SE (375×667, 60Hz)** — both in-batch controls
|
|
155
|
+
held 60.0/0 (zero drift): ship 59.6 UI / 0 UI jank; ×12 58.8 / 1; ×30 breaks
|
|
156
|
+
(54.4 UI, CPU 57). Same ranking as the sim; the sim → device carry-over held.
|
|
157
|
+
|
|
158
|
+
**Animated vs static (real iPhone, same batch):** identical — 59.6 UI / 0
|
|
159
|
+
jank / 0 JS jank both; static saved ~2.7 CPU points, less than the batch's
|
|
160
|
+
own control-to-control CPU spread (4.4), though the direction repeated on the
|
|
161
|
+
sim (~2.4). The motion toggle is a preference, not a perf lever, at shipping
|
|
162
|
+
scale — which is the native-driver contract holding. (A JS-jank scare in an
|
|
163
|
+
earlier batch attributed ~3 events to the ml cases; a later batch put 4 on a
|
|
164
|
+
CONTROL, so those are between-case reload noise.)
|
|
165
|
+
|
|
166
|
+
**Benchmark-protocol note:** the batch's FINAL case is unreliable — in two
|
|
167
|
+
consecutive batches the trailing control recorded byte-identical bad numbers
|
|
168
|
+
(53.7 UI FPS, 4 JS jank, only 2/3 runs) while an identical control at slot 1
|
|
169
|
+
was clean, so the batch teardown bleeds into the last recording. End every
|
|
170
|
+
batch with a sacrificial case, the same way it starts with one.
|
|
171
|
+
|
|
90
172
|
**Mount** — the cost that is real for all of them:
|
|
91
173
|
|
|
92
174
|
```
|
|
@@ -110,6 +192,6 @@ Add the component to `variants/`, then one entry in `registry.tsx` with an
|
|
|
110
192
|
honest `nodes` (animated nodes) and `views` (static views at ~380×620). The
|
|
111
193
|
switcher shows both, which is the point — it's a review tool, not decoration.
|
|
112
194
|
|
|
113
|
-
Alpha calibration:
|
|
114
|
-
disappears under
|
|
115
|
-
or a second pixel of height to register at all.
|
|
195
|
+
Alpha calibration: every preset runs a 0.4–0.55 vignette, and anything fainter
|
|
196
|
+
than ~0.2 alpha disappears under it — hairline strokes in particular need ~0.2
|
|
197
|
+
alpha or a second pixel of height to register at all.
|
|
@@ -14,7 +14,20 @@ import { useEffect, useState } from "react";
|
|
|
14
14
|
import { devToolsStorageKeys } from "../../storage/devToolsStorageKeys.js";
|
|
15
15
|
import { persistentStorage } from "../../utils/persistentStorage.js";
|
|
16
16
|
import { BACKGROUND_PRESETS, DEFAULT_BACKGROUND, presetAt, presetIndex } from "./registry.js";
|
|
17
|
-
|
|
17
|
+
// v3: bumped when the default became "off" (backgrounds are opt-in now), so
|
|
18
|
+
// devices that stepped the switcher during the (unreleased) audition rounds
|
|
19
|
+
// re-default to none instead of keeping a pre-release pick forever. Earlier
|
|
20
|
+
// keys are simply abandoned, same as v1 was.
|
|
21
|
+
const KEY = `${devToolsStorageKeys.base}_tool_background_v3`;
|
|
22
|
+
|
|
23
|
+
/** The persisted-selection key, exported so a host can ask "has the user ever
|
|
24
|
+
* picked a background?" (the desktop's first-run hint modal reads it). */
|
|
25
|
+
export const BACKGROUND_STORAGE_KEY = KEY;
|
|
26
|
+
|
|
27
|
+
// Motion is deliberately NOT persisted: the switcher no longer offers a
|
|
28
|
+
// motion toggle, so every session starts animated. `setMotion` survives as a
|
|
29
|
+
// session-only seam (bench harnesses, a tool forcing stillness).
|
|
30
|
+
|
|
18
31
|
function isValidId(value) {
|
|
19
32
|
return typeof value === "string" && BACKGROUND_PRESETS.some(p => p.id === value);
|
|
20
33
|
}
|
|
@@ -47,8 +60,10 @@ class BackgroundStore {
|
|
|
47
60
|
const raw = await persistentStorage.getItem(KEY);
|
|
48
61
|
if (!raw || this.dirty) return;
|
|
49
62
|
const parsed = JSON.parse(raw);
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
// The standalone Meteors preset was folded into Deep Field — re-home
|
|
64
|
+
// anyone still persisted on it rather than dropping them to the default.
|
|
65
|
+
const id = parsed.id === "meteors" ? "deepfield" : parsed.id;
|
|
66
|
+
if (isValidId(id)) this.id = id;
|
|
52
67
|
this.notify();
|
|
53
68
|
} catch {
|
|
54
69
|
// A missing or corrupt entry just means "use the default".
|
|
@@ -59,8 +74,7 @@ class BackgroundStore {
|
|
|
59
74
|
persist() {
|
|
60
75
|
this.dirty = true;
|
|
61
76
|
const payload = {
|
|
62
|
-
id: this.id
|
|
63
|
-
motion: this.motion
|
|
77
|
+
id: this.id
|
|
64
78
|
};
|
|
65
79
|
void persistentStorage.setItem(KEY, JSON.stringify(payload)).catch(() => {});
|
|
66
80
|
}
|
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
export { ToolBackground } from "./ToolBackground.js";
|
|
4
4
|
export { BackgroundSwitcher } from "./BackgroundSwitcher.js";
|
|
5
|
-
export { backgroundStore, useToolBackground } from "./backgroundStore.js";
|
|
5
|
+
export { backgroundStore, BACKGROUND_STORAGE_KEY, useToolBackground } from "./backgroundStore.js";
|
|
6
6
|
export { BACKGROUND_PRESETS, DEFAULT_BACKGROUND, getPreset, presetAt, presetIndex } from "./registry.js";
|
|
7
|
+
// Drawing/animation primitives, exported so other Buoy chrome (the dial menu's
|
|
8
|
+
// snow effect in @buoy-gg/core) can build matching effects under the same
|
|
9
|
+
// native-driven performance contract instead of copying the implementations.
|
|
10
|
+
export { LinearFade, RadialGlow, RasterLayer, useLoop, usePulse } from "./primitives.js";
|
|
11
|
+
export { scatter, withAlpha as withBgAlpha } from "./rng.js";
|
|
7
12
|
// Exported for the bg-bench harness in example-dev-build, so every measured
|
|
8
13
|
// variant is the shipping component with knobs turned rather than a copy.
|
|
9
|
-
export { DeepFieldTunable } from "./variants/deepField.js";
|
|
14
|
+
export { DeepFieldTunable } from "./variants/deepField.js";
|
|
15
|
+
export { MoonlightTunable } from "./variants/moonlight.js";
|
|
16
|
+
export { DaylightTunable } from "./variants/daylight.js";
|
|
17
|
+
export { LiveSkyTunable } from "./variants/liveSky.js";
|
|
18
|
+
export { getSkyScrub, setSkyScrub, useSkyScrub } from "./skyClock.js";
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* looping `Animated.timing` means the whole loop is handed to the UI thread
|
|
16
16
|
* once and never touches JS again — no per-frame callback, no re-render, no
|
|
17
17
|
* bridge traffic while it runs. A preset animates a handful of *layers*
|
|
18
|
-
* (transform on one parent view), never per-particle.
|
|
18
|
+
* (transform on one parent view), never per-particle. (On web there is no
|
|
19
|
+
* native driver and the JS driver must be requested explicitly — see
|
|
20
|
+
* NATIVE_DRIVER below for why the flag can't simply be left `true`.)
|
|
19
21
|
*
|
|
20
22
|
* 2. **Every static layer is rasterized.** `shouldRasterizeIOS` /
|
|
21
23
|
* `renderToHardwareTextureAndroid` turn a layer of 40 dots into a single
|
|
@@ -29,21 +31,35 @@
|
|
|
29
31
|
import { memo, useEffect, useRef } from "react";
|
|
30
32
|
import { Animated, AppState, Easing, Platform, StyleSheet, View } from "react-native";
|
|
31
33
|
import { absoluteFill } from "../../utils/absoluteFill.js";
|
|
32
|
-
import { withAlpha } from "./rng.js";
|
|
34
|
+
import { flattenOver, withAlpha } from "./rng.js";
|
|
33
35
|
|
|
34
36
|
// ============================================================================
|
|
35
37
|
// Animation
|
|
36
38
|
// ============================================================================
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
|
-
*
|
|
41
|
+
* Ask for the native driver ONLY where the native animated module exists.
|
|
42
|
+
*
|
|
43
|
+
* On react-native-web (the desktop dashboard) `useNativeDriver: true` is
|
|
44
|
+
* demoted to the JS driver — but `Animated.loop` trusts the config flag, not
|
|
45
|
+
* the demotion: for a bare `loop(timing(...))` it takes its native-loop
|
|
46
|
+
* shortcut and starts ONE plain timing whose `iterations: -1` only the
|
|
47
|
+
* (absent) native module would honor. The loop plays a single pass and
|
|
48
|
+
* freezes — on desktop every background stopped seconds after opening.
|
|
49
|
+
* Requesting the JS driver honestly keeps loop's JS restart path in charge,
|
|
50
|
+
* which is the only driver web has anyway.
|
|
51
|
+
*/
|
|
52
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
53
|
+
const NATIVE_DRIVER = Platform.OS !== "web";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A native-driven 0→1 loop (JS-driven on web — see NATIVE_DRIVER).
|
|
40
57
|
*
|
|
41
58
|
* The returned value is only ever consumed by `interpolate` inside an
|
|
42
59
|
* `Animated.View` style, so React never re-renders while it runs. The loop is
|
|
43
60
|
* stopped when the app backgrounds (a background animation is pure battery
|
|
44
61
|
* burn) and restarted on foreground.
|
|
45
62
|
*/
|
|
46
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
47
63
|
export function useLoop(durationMs, enabled = true) {
|
|
48
64
|
const value = useRef(new Animated.Value(0)).current;
|
|
49
65
|
useEffect(() => {
|
|
@@ -56,7 +72,11 @@ export function useLoop(durationMs, enabled = true) {
|
|
|
56
72
|
toValue: 1,
|
|
57
73
|
duration: durationMs,
|
|
58
74
|
easing: Easing.linear,
|
|
59
|
-
useNativeDriver:
|
|
75
|
+
useNativeDriver: NATIVE_DRIVER,
|
|
76
|
+
// A decorative endless loop must never register as an interaction —
|
|
77
|
+
// the default (true) would block InteractionManager callbacks in the
|
|
78
|
+
// host app for as long as the background is mounted.
|
|
79
|
+
isInteraction: false
|
|
60
80
|
}));
|
|
61
81
|
anim.start();
|
|
62
82
|
};
|
|
@@ -93,12 +113,16 @@ export function usePulse(durationMs, enabled = true) {
|
|
|
93
113
|
toValue: 1,
|
|
94
114
|
duration: durationMs / 2,
|
|
95
115
|
easing: Easing.inOut(Easing.quad),
|
|
96
|
-
useNativeDriver:
|
|
116
|
+
useNativeDriver: NATIVE_DRIVER,
|
|
117
|
+
// See useLoop: endless decorative loops must not hold
|
|
118
|
+
// InteractionManager (isInteraction defaults to true).
|
|
119
|
+
isInteraction: false
|
|
97
120
|
}), Animated.timing(value, {
|
|
98
121
|
toValue: 0,
|
|
99
122
|
duration: durationMs / 2,
|
|
100
123
|
easing: Easing.inOut(Easing.quad),
|
|
101
|
-
useNativeDriver:
|
|
124
|
+
useNativeDriver: NATIVE_DRIVER,
|
|
125
|
+
isInteraction: false
|
|
102
126
|
})]));
|
|
103
127
|
anim.start();
|
|
104
128
|
};
|
|
@@ -193,7 +217,18 @@ export const RadialGlow = /*#__PURE__*/memo(function RadialGlow({
|
|
|
193
217
|
/**
|
|
194
218
|
* Fake linear gradient: `bands` stacked strips with linearly interpolated
|
|
195
219
|
* alpha. Used for scrims (keep text readable over a busy field) and for
|
|
196
|
-
* horizon glows.
|
|
220
|
+
* horizon glows.
|
|
221
|
+
*
|
|
222
|
+
* TRAP (found on the Abyss wash): translucent strips must NOT overlap. An
|
|
223
|
+
* overlap composites the same colour twice, doubling the alpha in a stripe at
|
|
224
|
+
* every band edge — on a dark surface that draws a visible line per band, and
|
|
225
|
+
* because the strips are %-sized the whole ruled pattern rescales as the
|
|
226
|
+
* modal resizes. Strips here butt exactly, with a 0.08% fudge (sub-pixel at
|
|
227
|
+
* any real size) so layout rounding can't open a hairline gap.
|
|
228
|
+
*
|
|
229
|
+
* `flattenBase`: when the fade sits DIRECTLY on a known opaque colour, pass
|
|
230
|
+
* that colour — each strip is then drawn opaque via `flattenOver`, which is
|
|
231
|
+
* pixel-identical, immune to seams entirely, and cheaper to composite.
|
|
197
232
|
*/
|
|
198
233
|
export const LinearFade = /*#__PURE__*/memo(function LinearFade({
|
|
199
234
|
color,
|
|
@@ -201,6 +236,7 @@ export const LinearFade = /*#__PURE__*/memo(function LinearFade({
|
|
|
201
236
|
to = 1,
|
|
202
237
|
bands = 14,
|
|
203
238
|
direction = "down",
|
|
239
|
+
flattenBase,
|
|
204
240
|
style
|
|
205
241
|
}) {
|
|
206
242
|
const vertical = direction === "down" || direction === "up";
|
|
@@ -210,21 +246,22 @@ export const LinearFade = /*#__PURE__*/memo(function LinearFade({
|
|
|
210
246
|
for (let i = 0; i < bands; i++) {
|
|
211
247
|
const t = (i + 0.5) / bands;
|
|
212
248
|
const a = from + (to - from) * (reversed ? 1 - t : t);
|
|
249
|
+
const fill = flattenBase ? flattenOver(color, a, flattenBase) : withAlpha(color, a);
|
|
213
250
|
strips.push(/*#__PURE__*/_jsx(View, {
|
|
214
251
|
style: vertical ? {
|
|
215
252
|
position: "absolute",
|
|
216
253
|
left: 0,
|
|
217
254
|
right: 0,
|
|
218
255
|
top: `${i * pct}%`,
|
|
219
|
-
height: `${pct +
|
|
220
|
-
backgroundColor:
|
|
256
|
+
height: `${pct + 0.08}%`,
|
|
257
|
+
backgroundColor: fill
|
|
221
258
|
} : {
|
|
222
259
|
position: "absolute",
|
|
223
260
|
top: 0,
|
|
224
261
|
bottom: 0,
|
|
225
262
|
left: `${i * pct}%`,
|
|
226
|
-
width: `${pct +
|
|
227
|
-
backgroundColor:
|
|
263
|
+
width: `${pct + 0.08}%`,
|
|
264
|
+
backgroundColor: fill
|
|
228
265
|
}
|
|
229
266
|
}, i));
|
|
230
267
|
}
|
|
@@ -263,7 +300,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
|
|
|
263
300
|
from: alpha,
|
|
264
301
|
to: 0,
|
|
265
302
|
direction: "down",
|
|
266
|
-
bands:
|
|
303
|
+
bands: 14
|
|
267
304
|
})
|
|
268
305
|
}), /*#__PURE__*/_jsx(View, {
|
|
269
306
|
style: {
|
|
@@ -278,7 +315,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
|
|
|
278
315
|
from: 0,
|
|
279
316
|
to: alpha,
|
|
280
317
|
direction: "down",
|
|
281
|
-
bands:
|
|
318
|
+
bands: 14
|
|
282
319
|
})
|
|
283
320
|
}), /*#__PURE__*/_jsx(View, {
|
|
284
321
|
style: {
|
|
@@ -293,7 +330,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
|
|
|
293
330
|
from: alpha,
|
|
294
331
|
to: 0,
|
|
295
332
|
direction: "right",
|
|
296
|
-
bands:
|
|
333
|
+
bands: 10
|
|
297
334
|
})
|
|
298
335
|
}), /*#__PURE__*/_jsx(View, {
|
|
299
336
|
style: {
|
|
@@ -308,7 +345,7 @@ export const Vignette = /*#__PURE__*/memo(function Vignette({
|
|
|
308
345
|
from: 0,
|
|
309
346
|
to: alpha,
|
|
310
347
|
direction: "right",
|
|
311
|
-
bands:
|
|
348
|
+
bands: 10
|
|
312
349
|
})
|
|
313
350
|
})]
|
|
314
351
|
});
|
|
@@ -10,88 +10,65 @@
|
|
|
10
10
|
* them honest.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { Crosshatch, Halftone, HexMesh, Instrument, IsoGrid, PerfBoard, Radar, Waffle } from "./variants/lattice.js";
|
|
14
13
|
import { DeepField } from "./variants/deepField.js";
|
|
15
|
-
import {
|
|
14
|
+
import { Hyperspace } from "./variants/hyperspace.js";
|
|
15
|
+
import { LiveSky } from "./variants/liveSky.js";
|
|
16
|
+
import { Abyss, Bubbles, Jellyfish } from "./variants/underwater.js";
|
|
16
17
|
export const BACKGROUND_PRESETS = [{
|
|
17
18
|
id: "off",
|
|
18
19
|
label: "Off",
|
|
19
|
-
blurb: "Plain black —
|
|
20
|
+
blurb: "Plain black — no backdrop at all",
|
|
20
21
|
nodes: 0,
|
|
21
22
|
views: 1,
|
|
22
23
|
Component: null
|
|
23
|
-
}, {
|
|
24
|
-
id: "starfield",
|
|
25
|
-
label: "Starfield",
|
|
26
|
-
blurb: "Five depth bands drifting at parallax speeds",
|
|
27
|
-
nodes: 6,
|
|
28
|
-
views: 222,
|
|
29
|
-
Component: Starfield
|
|
30
24
|
}, {
|
|
31
25
|
id: "deepfield",
|
|
32
26
|
label: "Deep Field",
|
|
33
|
-
blurb: "The Everlights field
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
blurb: "The Everlights field — 160 stars, with meteors slamming the walls",
|
|
28
|
+
// The old standalone Meteors preset folded in: +2 meteor loops, ~50 views.
|
|
29
|
+
nodes: 24,
|
|
30
|
+
views: 510,
|
|
36
31
|
Component: DeepField
|
|
37
32
|
}, {
|
|
38
|
-
id: "
|
|
39
|
-
label: "
|
|
40
|
-
blurb: "
|
|
41
|
-
nodes: 1,
|
|
42
|
-
views: 160,
|
|
43
|
-
Component: HexMesh
|
|
44
|
-
}, {
|
|
45
|
-
id: "isogrid",
|
|
46
|
-
label: "Iso Grid",
|
|
47
|
-
blurb: "Three-axis isometric ruling",
|
|
33
|
+
id: "hyperspace",
|
|
34
|
+
label: "Hyperspace",
|
|
35
|
+
blurb: "Lightspeed — crisp points streaming from a centre vanishing point",
|
|
48
36
|
nodes: 1,
|
|
49
|
-
views:
|
|
50
|
-
Component:
|
|
37
|
+
views: 570,
|
|
38
|
+
Component: Hyperspace
|
|
51
39
|
}, {
|
|
52
|
-
id: "
|
|
53
|
-
label: "
|
|
54
|
-
blurb: "
|
|
55
|
-
nodes:
|
|
56
|
-
views:
|
|
57
|
-
Component:
|
|
40
|
+
id: "livesky",
|
|
41
|
+
label: "Live Sky",
|
|
42
|
+
blurb: "The cove on your clock — dawn to dusk, for real",
|
|
43
|
+
nodes: 5,
|
|
44
|
+
views: 290,
|
|
45
|
+
Component: LiveSky
|
|
58
46
|
}, {
|
|
59
|
-
id: "
|
|
60
|
-
label: "
|
|
61
|
-
blurb: "
|
|
62
|
-
nodes:
|
|
63
|
-
views:
|
|
64
|
-
Component:
|
|
47
|
+
id: "abyss",
|
|
48
|
+
label: "Abyss",
|
|
49
|
+
blurb: "A surface light fanning into the deep",
|
|
50
|
+
nodes: 4,
|
|
51
|
+
views: 230,
|
|
52
|
+
Component: Abyss
|
|
65
53
|
}, {
|
|
66
|
-
id: "
|
|
67
|
-
label: "
|
|
68
|
-
blurb: "
|
|
69
|
-
nodes:
|
|
70
|
-
views:
|
|
71
|
-
Component:
|
|
54
|
+
id: "bubbles",
|
|
55
|
+
label: "Bubbles",
|
|
56
|
+
blurb: "Bubbles wobbling up through the light",
|
|
57
|
+
nodes: 4,
|
|
58
|
+
views: 180,
|
|
59
|
+
Component: Bubbles
|
|
72
60
|
}, {
|
|
73
|
-
id: "
|
|
74
|
-
label: "
|
|
75
|
-
blurb: "
|
|
76
|
-
nodes:
|
|
77
|
-
views:
|
|
78
|
-
Component:
|
|
79
|
-
}, {
|
|
80
|
-
id: "instrument",
|
|
81
|
-
label: "Instrument",
|
|
82
|
-
blurb: "Ruled scales with major graduations",
|
|
83
|
-
nodes: 1,
|
|
84
|
-
views: 110,
|
|
85
|
-
Component: Instrument
|
|
86
|
-
}, {
|
|
87
|
-
id: "radar",
|
|
88
|
-
label: "Radar",
|
|
89
|
-
blurb: "Rings and spokes under a rotating sweep",
|
|
90
|
-
nodes: 2,
|
|
91
|
-
views: 40,
|
|
92
|
-
Component: Radar
|
|
61
|
+
id: "jellyfish",
|
|
62
|
+
label: "Jellyfish",
|
|
63
|
+
blurb: "A bloom of glowing jellies, big and small, pulsing past",
|
|
64
|
+
nodes: 15,
|
|
65
|
+
views: 250,
|
|
66
|
+
Component: Jellyfish
|
|
93
67
|
}];
|
|
94
|
-
|
|
68
|
+
|
|
69
|
+
// Ships OFF: a backdrop is an opt-in, so first load is plain black and the
|
|
70
|
+
// choice only persists once the user actually picks one in the switcher.
|
|
71
|
+
export const DEFAULT_BACKGROUND = "off";
|
|
95
72
|
export function presetIndex(id) {
|
|
96
73
|
const i = BACKGROUND_PRESETS.findIndex(p => p.id === id);
|
|
97
74
|
return i < 0 ? 0 : i;
|
|
@@ -42,6 +42,26 @@ export function withAlpha(hex, alpha) {
|
|
|
42
42
|
return `rgba(${r},${g},${b},${a.toFixed(3)})`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* RGB lerp between two `#RRGGBB` (or `#RGB`) colours, returned as `#RRGGBB`.
|
|
47
|
+
* Used by Live Sky to blend palette keyframes; at minute-sized steps a plain
|
|
48
|
+
* RGB lerp is indistinguishable from anything fancier.
|
|
49
|
+
*/
|
|
50
|
+
export function lerpColor(a, b, t) {
|
|
51
|
+
const parse = h => {
|
|
52
|
+
let v = h.replace("#", "");
|
|
53
|
+
if (v.length === 3) v = v[0] + v[0] + v[1] + v[1] + v[2] + v[2];
|
|
54
|
+
const n = parseInt(v, 16);
|
|
55
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
56
|
+
};
|
|
57
|
+
const [ar, ag, ab] = parse(a);
|
|
58
|
+
const [br, bg, bb] = parse(b);
|
|
59
|
+
const c = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
60
|
+
const mix = (x, y) => Math.round(x + (y - x) * c);
|
|
61
|
+
const hex = n => n.toString(16).padStart(2, "0");
|
|
62
|
+
return `#${hex(mix(ar, br))}${hex(mix(ag, bg))}${hex(mix(ab, bb))}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
/**
|
|
46
66
|
* `hex` at `alpha` composited over an opaque `base`, returned as an OPAQUE
|
|
47
67
|
* colour.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Live Sky scrub clock.
|
|
5
|
+
*
|
|
6
|
+
* `null` means "live" — the Live Sky preset follows the real local clock. A
|
|
7
|
+
* number (minutes since midnight, quantized by the slider) freezes the scene
|
|
8
|
+
* at that time of day so the whole cycle can be scrubbed through by hand.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately NOT part of `backgroundStore`: the variant itself has to read
|
|
11
|
+
* this, and a variant importing the store would close an import cycle through
|
|
12
|
+
* the registry (store → registry → variant → store). It is also deliberately
|
|
13
|
+
* not persisted — a scrub is a preview gesture, not a setting.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useState } from "react";
|
|
17
|
+
let scrub = null;
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
export function getSkyScrub() {
|
|
20
|
+
return scrub;
|
|
21
|
+
}
|
|
22
|
+
export function setSkyScrub(value) {
|
|
23
|
+
if (value === scrub) return;
|
|
24
|
+
scrub = value;
|
|
25
|
+
for (const listener of listeners) listener();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Subscribe a component to the scrub value. */
|
|
29
|
+
export function useSkyScrub() {
|
|
30
|
+
const [value, setValue] = useState(scrub);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const listener = () => setValue(scrub);
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
return () => {
|
|
35
|
+
listeners.delete(listener);
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
return value;
|
|
39
|
+
}
|