react-native-webrtc-kaleidoscope 2.2.1 → 2.3.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.
Files changed (51) hide show
  1. package/README.md +30 -0
  2. package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/ShadersGenerated.kt +57 -23
  3. package/catalog/shaders/clouds/clouds.frag +21 -6
  4. package/catalog/shaders/nebula/nebula.frag +22 -10
  5. package/catalog/shaders/simianlights/simianlights.frag +19 -9
  6. package/dist/src/components/preset-control-panel/preset-control-panel.d.ts +9 -1
  7. package/dist/src/components/preset-control-panel/preset-control-panel.d.ts.map +1 -1
  8. package/dist/src/components/preset-control-panel/preset-control-panel.js +7 -3
  9. package/dist/src/components/preset-control-panel/preset-control-panel.js.map +1 -1
  10. package/dist/src/kaleidoscope/controls.d.ts.map +1 -1
  11. package/dist/src/kaleidoscope/controls.js +6 -3
  12. package/dist/src/kaleidoscope/controls.js.map +1 -1
  13. package/dist/src/kaleidoscope/shader-to-spec.d.ts +15 -1
  14. package/dist/src/kaleidoscope/shader-to-spec.d.ts.map +1 -1
  15. package/dist/src/kaleidoscope/shader-to-spec.js +23 -4
  16. package/dist/src/kaleidoscope/shader-to-spec.js.map +1 -1
  17. package/dist/src/kaleidoscope/types.d.ts +3 -1
  18. package/dist/src/kaleidoscope/types.d.ts.map +1 -1
  19. package/dist/src/kaleidoscope/types.js.map +1 -1
  20. package/dist/src/persistence/async-storage-store.d.ts +3 -0
  21. package/dist/src/persistence/async-storage-store.d.ts.map +1 -0
  22. package/dist/src/persistence/async-storage-store.js +26 -0
  23. package/dist/src/persistence/async-storage-store.js.map +1 -0
  24. package/dist/src/persistence/index.d.ts +4 -0
  25. package/dist/src/persistence/index.d.ts.map +1 -0
  26. package/dist/src/persistence/index.js +22 -0
  27. package/dist/src/persistence/index.js.map +1 -0
  28. package/dist/src/persistence/provider.d.ts +38 -0
  29. package/dist/src/persistence/provider.d.ts.map +1 -0
  30. package/dist/src/persistence/provider.js +96 -0
  31. package/dist/src/persistence/provider.js.map +1 -0
  32. package/dist/src/persistence/state.d.ts +54 -0
  33. package/dist/src/persistence/state.d.ts.map +1 -0
  34. package/dist/src/persistence/state.js +126 -0
  35. package/dist/src/persistence/state.js.map +1 -0
  36. package/dist/web-driver/shaders.generated.d.ts +3 -3
  37. package/dist/web-driver/shaders.generated.d.ts.map +1 -1
  38. package/dist/web-driver/shaders.generated.js +57 -23
  39. package/dist/web-driver/shaders.generated.js.map +1 -1
  40. package/ios/KaleidoscopeModule/shaders/clouds.metalsrc +100 -99
  41. package/ios/KaleidoscopeModule/shaders/nebula.metalsrc +63 -45
  42. package/ios/KaleidoscopeModule/shaders/simianlights.metalsrc +63 -45
  43. package/package.json +14 -2
  44. package/src/components/preset-control-panel/preset-control-panel.tsx +15 -2
  45. package/src/kaleidoscope/controls.ts +6 -3
  46. package/src/kaleidoscope/shader-to-spec.ts +32 -5
  47. package/src/kaleidoscope/types.ts +3 -1
  48. package/src/persistence/async-storage-store.ts +33 -0
  49. package/src/persistence/index.ts +28 -0
  50. package/src/persistence/provider.tsx +165 -0
  51. package/src/persistence/state.ts +167 -0
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ // The default backing store: AsyncStorage under the canonical key. Works on
3
+ // every platform (the web build is localStorage-backed).
4
+ //
5
+ // `@react-native-async-storage/async-storage` is an OPTIONAL peer dependency of
6
+ // the package: only the `/persistence` subpath touches it, and Metro resolves
7
+ // it at bundle time for any app that imports this subpath (even with a custom
8
+ // `store`; bundlers do not tree-shake the default away).
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.kaleidoscopeAsyncStorageStore = void 0;
14
+ const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
15
+ const state_1 = require("./state");
16
+ exports.kaleidoscopeAsyncStorageStore = {
17
+ load() {
18
+ return async_storage_1.default.getItem(state_1.KALEIDOSCOPE_STATE_KEY).then(state_1.parseStoredKaleidoscopeState, () => null);
19
+ },
20
+ save(state) {
21
+ return async_storage_1.default.setItem(state_1.KALEIDOSCOPE_STATE_KEY, (0, state_1.serializeKaleidoscopeState)(state)).then(() => undefined, (error) => {
22
+ console.warn('kaleidoscope: preset persistence save failed', error);
23
+ });
24
+ },
25
+ };
26
+ //# sourceMappingURL=async-storage-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-storage-store.js","sourceRoot":"","sources":["../../../src/persistence/async-storage-store.ts"],"names":[],"mappings":";AAAA,4EAA4E;AAC5E,yDAAyD;AACzD,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,8EAA8E;AAC9E,yDAAyD;;;;;;AAEzD,8FAAqE;AACrE,mCAMiB;AAEJ,QAAA,6BAA6B,GAA2B;IACnE,IAAI;QACF,OAAO,uBAAY,CAAC,OAAO,CAAC,8BAAsB,CAAC,CAAC,IAAI,CACtD,oCAA4B,EAC5B,GAAG,EAAE,CAAC,IAAI,CACX,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAA8B;QACjC,OAAO,uBAAY,CAAC,OAAO,CAAC,8BAAsB,EAAE,IAAA,kCAA0B,EAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACzF,GAAG,EAAE,CAAC,SAAS,EACf,CAAC,KAAK,EAAE,EAAE;YACR,OAAO,CAAC,IAAI,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;QACtE,CAAC,CACF,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["// The default backing store: AsyncStorage under the canonical key. Works on\n// every platform (the web build is localStorage-backed).\n//\n// `@react-native-async-storage/async-storage` is an OPTIONAL peer dependency of\n// the package: only the `/persistence` subpath touches it, and Metro resolves\n// it at bundle time for any app that imports this subpath (even with a custom\n// `store`; bundlers do not tree-shake the default away).\n\nimport AsyncStorage from '@react-native-async-storage/async-storage';\nimport {\n KALEIDOSCOPE_STATE_KEY,\n type KaleidoscopeStateStore,\n parseStoredKaleidoscopeState,\n type StoredKaleidoscopeState,\n serializeKaleidoscopeState,\n} from './state';\n\nexport const kaleidoscopeAsyncStorageStore: KaleidoscopeStateStore = {\n load(): Promise<StoredKaleidoscopeState | null> {\n return AsyncStorage.getItem(KALEIDOSCOPE_STATE_KEY).then(\n parseStoredKaleidoscopeState,\n () => null,\n );\n },\n save(state: StoredKaleidoscopeState): Promise<void> {\n return AsyncStorage.setItem(KALEIDOSCOPE_STATE_KEY, serializeKaleidoscopeState(state)).then(\n () => undefined,\n (error) => {\n console.warn('kaleidoscope: preset persistence save failed', error);\n },\n );\n },\n};\n"]}
@@ -0,0 +1,4 @@
1
+ export { kaleidoscopeAsyncStorageStore } from './async-storage-store';
2
+ export { KaleidoscopeStateProvider, type KaleidoscopeStateProviderProps, type KaleidoscopeStateValue, useKaleidoscopeState, } from './provider';
3
+ export { DEFAULT_MASK, KALEIDOSCOPE_STATE_KEY, type KaleidoscopeStateStore, parseStoredKaleidoscopeState, pruneStoredState, type StoredKaleidoscopeState, type StoredLayerUniforms, type StoredPatch, type StoredPatches, type StoredPatchMap, serializeKaleidoscopeState, } from './state';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/persistence/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,6BAA6B,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EACL,yBAAyB,EACzB,KAAK,8BAA8B,EACnC,KAAK,sBAAsB,EAC3B,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,0BAA0B,GAC3B,MAAM,SAAS,CAAC"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ // Subpath entry: react-native-webrtc-kaleidoscope/persistence
3
+ //
4
+ // The persisted-selection convenience: a provider + hook that keep the last
5
+ // applied preset, its control-panel patches, and the mask across launches.
6
+ // Storage-agnostic via `KaleidoscopeStateStore`; defaults to AsyncStorage
7
+ // (`@react-native-async-storage/async-storage`, an optional peer dependency
8
+ // required only by apps that use the default store).
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.serializeKaleidoscopeState = exports.pruneStoredState = exports.parseStoredKaleidoscopeState = exports.KALEIDOSCOPE_STATE_KEY = exports.DEFAULT_MASK = exports.useKaleidoscopeState = exports.KaleidoscopeStateProvider = exports.kaleidoscopeAsyncStorageStore = void 0;
11
+ var async_storage_store_1 = require("./async-storage-store");
12
+ Object.defineProperty(exports, "kaleidoscopeAsyncStorageStore", { enumerable: true, get: function () { return async_storage_store_1.kaleidoscopeAsyncStorageStore; } });
13
+ var provider_1 = require("./provider");
14
+ Object.defineProperty(exports, "KaleidoscopeStateProvider", { enumerable: true, get: function () { return provider_1.KaleidoscopeStateProvider; } });
15
+ Object.defineProperty(exports, "useKaleidoscopeState", { enumerable: true, get: function () { return provider_1.useKaleidoscopeState; } });
16
+ var state_1 = require("./state");
17
+ Object.defineProperty(exports, "DEFAULT_MASK", { enumerable: true, get: function () { return state_1.DEFAULT_MASK; } });
18
+ Object.defineProperty(exports, "KALEIDOSCOPE_STATE_KEY", { enumerable: true, get: function () { return state_1.KALEIDOSCOPE_STATE_KEY; } });
19
+ Object.defineProperty(exports, "parseStoredKaleidoscopeState", { enumerable: true, get: function () { return state_1.parseStoredKaleidoscopeState; } });
20
+ Object.defineProperty(exports, "pruneStoredState", { enumerable: true, get: function () { return state_1.pruneStoredState; } });
21
+ Object.defineProperty(exports, "serializeKaleidoscopeState", { enumerable: true, get: function () { return state_1.serializeKaleidoscopeState; } });
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/persistence/index.ts"],"names":[],"mappings":";AAAA,8DAA8D;AAC9D,EAAE;AACF,4EAA4E;AAC5E,2EAA2E;AAC3E,0EAA0E;AAC1E,4EAA4E;AAC5E,qDAAqD;;;AAErD,6DAAsE;AAA7D,oIAAA,6BAA6B,OAAA;AACtC,uCAKoB;AAJlB,qHAAA,yBAAyB,OAAA;AAGzB,gHAAA,oBAAoB,OAAA;AAEtB,iCAYiB;AAXf,qGAAA,YAAY,OAAA;AACZ,+GAAA,sBAAsB,OAAA;AAEtB,qHAAA,4BAA4B,OAAA;AAC5B,yGAAA,gBAAgB,OAAA;AAMhB,mHAAA,0BAA0B,OAAA","sourcesContent":["// Subpath entry: react-native-webrtc-kaleidoscope/persistence\n//\n// The persisted-selection convenience: a provider + hook that keep the last\n// applied preset, its control-panel patches, and the mask across launches.\n// Storage-agnostic via `KaleidoscopeStateStore`; defaults to AsyncStorage\n// (`@react-native-async-storage/async-storage`, an optional peer dependency\n// required only by apps that use the default store).\n\nexport { kaleidoscopeAsyncStorageStore } from './async-storage-store';\nexport {\n KaleidoscopeStateProvider,\n type KaleidoscopeStateProviderProps,\n type KaleidoscopeStateValue,\n useKaleidoscopeState,\n} from './provider';\nexport {\n DEFAULT_MASK,\n KALEIDOSCOPE_STATE_KEY,\n type KaleidoscopeStateStore,\n parseStoredKaleidoscopeState,\n pruneStoredState,\n type StoredKaleidoscopeState,\n type StoredLayerUniforms,\n type StoredPatch,\n type StoredPatches,\n type StoredPatchMap,\n serializeKaleidoscopeState,\n} from './state';\n"]}
@@ -0,0 +1,38 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ import type { MaskInput, PatchesFor } from '../kaleidoscope/types';
3
+ import type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';
4
+ import { type KaleidoscopeStateStore, type StoredPatch, type StoredPatches } from './state';
5
+ export type KaleidoscopeStateValue<P extends KaleidoscopePresetBook = KaleidoscopePresetBook> = {
6
+ /** False until the persisted selection has been read; apply no effects before then. */
7
+ readonly hydrated: boolean;
8
+ /** The selected preset id, or null when nothing is selected. */
9
+ readonly presetId: (keyof P & string) | null;
10
+ /** The shared segmentation edge. */
11
+ readonly mask: MaskInput;
12
+ /** Every preset's stored per-layer overrides (keyed by preset id, then layer id). */
13
+ readonly patches: StoredPatches;
14
+ readonly setPreset: (presetId: (keyof P & string) | null) => void;
15
+ readonly setMask: (mask: MaskInput) => void;
16
+ /** Record one control-panel patch against a preset (and persist it). */
17
+ readonly setPatch: (presetId: keyof P & string, patch: StoredPatch) => void;
18
+ /** A preset's stored overrides in the array shape `kaleidoscope(id, patches)` takes. */
19
+ readonly patchesFor: <K extends keyof P & string>(presetId: K) => PatchesFor<P, K>;
20
+ /** Clear the stored selection back to defaults (and persist the cleared state). */
21
+ readonly reset: () => void;
22
+ };
23
+ export type KaleidoscopeStateProviderProps<P extends KaleidoscopePresetBook> = {
24
+ /** The consumer's preset book; stored state is pruned against it at hydrate. */
25
+ readonly presets: P;
26
+ /** The backing store. Defaults to the AsyncStorage store (lazily loaded). */
27
+ readonly store?: KaleidoscopeStateStore;
28
+ /** The mask used before hydration and after `reset`. Defaults to 0.5/0.5. */
29
+ readonly defaultMask?: MaskInput;
30
+ readonly children: ReactNode;
31
+ };
32
+ export declare function KaleidoscopeStateProvider<P extends KaleidoscopePresetBook>({ presets, store, defaultMask, children, }: KaleidoscopeStateProviderProps<P>): ReactElement;
33
+ /**
34
+ * The persisted selection plus its setters. Typed by the consumer's book:
35
+ * `useKaleidoscopeState<typeof presets>()`. Throws outside the provider.
36
+ */
37
+ export declare function useKaleidoscopeState<P extends KaleidoscopePresetBook = KaleidoscopePresetBook>(): KaleidoscopeStateValue<P>;
38
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/persistence/provider.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,SAAS,EAKf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAEhF,OAAO,EAEL,KAAK,sBAAsB,EAI3B,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,sBAAsB,GAAG,sBAAsB,IAAI;IAC9F,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7C,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAClE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IAC5E,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACnF,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAIF,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,sBAAsB,IAAI;IAC7E,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,sBAAsB,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAC9B,CAAC;AAQF,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,sBAAsB,EAAE,EAC1E,OAAO,EACP,KAAK,EACL,WAA0B,EAC1B,QAAQ,GACT,EAAE,8BAA8B,CAAC,CAAC,CAAC,GAAG,YAAY,CA4DlD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,SAAS,sBAAsB,GAAG,sBAAsB,KACtD,sBAAsB,CAAC,CAAC,CAAC,CAY7B"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KaleidoscopeStateProvider = KaleidoscopeStateProvider;
4
+ exports.useKaleidoscopeState = useKaleidoscopeState;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ // Persistence: the React surface. `KaleidoscopeStateProvider` hydrates the
7
+ // stored selection once at mount and writes through on every change;
8
+ // `useKaleidoscopeState` hands the host the hydrated values plus the setters.
9
+ //
10
+ // The provider owns STORAGE state only; it never binds a track or calls the
11
+ // verbs. The host applies the restored selection itself (gated on `hydrated`,
12
+ // so a stored preset is not flashed over by the default):
13
+ //
14
+ // const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();
15
+ // useEffect(() => {
16
+ // if (!hydrated || !controls) return;
17
+ // if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));
18
+ // else controls.kaleidoscope(null);
19
+ // }, [hydrated, controls, presetId]);
20
+ //
21
+ // The backing store defaults to AsyncStorage; pass any `KaleidoscopeStateStore`
22
+ // to swap it. Importing this subpath is what brings the optional
23
+ // `@react-native-async-storage/async-storage` peer onto your bundle path
24
+ // (Metro resolves it at bundle time either way, so there is no lazy escape).
25
+ const react_1 = require("react");
26
+ const async_storage_store_1 = require("./async-storage-store");
27
+ const state_1 = require("./state");
28
+ const KaleidoscopeStateContext = (0, react_1.createContext)(null);
29
+ function KaleidoscopeStateProvider({ presets, store, defaultMask = state_1.DEFAULT_MASK, children, }) {
30
+ const [hydrated, setHydrated] = (0, react_1.useState)(false);
31
+ const [selection, setSelection] = (0, react_1.useState)({
32
+ presetId: null,
33
+ mask: defaultMask,
34
+ patches: {},
35
+ });
36
+ // A write before hydration wins over the stored value (the person acted; do
37
+ // not clobber their fresh choice with yesterday's).
38
+ const dirty = (0, react_1.useRef)(false);
39
+ // Pinned for the provider's lifetime: hydrate and every write-through go to
40
+ // the same store the first render saw.
41
+ const storeRef = (0, react_1.useRef)(store ?? async_storage_store_1.kaleidoscopeAsyncStorageStore);
42
+ // biome-ignore lint/correctness/useExhaustiveDependencies: hydrate exactly once; the book and store are bind-time constants.
43
+ (0, react_1.useEffect)(() => {
44
+ let cancelled = false;
45
+ storeRef.current.load().then((stored) => {
46
+ if (cancelled)
47
+ return;
48
+ if (stored && !dirty.current) {
49
+ const pruned = (0, state_1.pruneStoredState)(stored, presets);
50
+ setSelection({ presetId: pruned.presetId, mask: pruned.mask, patches: pruned.patches });
51
+ }
52
+ setHydrated(true);
53
+ }, () => {
54
+ if (!cancelled)
55
+ setHydrated(true);
56
+ });
57
+ return () => {
58
+ cancelled = true;
59
+ };
60
+ }, []);
61
+ const commit = (next) => {
62
+ dirty.current = true;
63
+ setSelection(next);
64
+ void storeRef.current.save({ version: 1, ...next });
65
+ };
66
+ const value = {
67
+ hydrated,
68
+ presetId: selection.presetId,
69
+ mask: selection.mask,
70
+ patches: selection.patches,
71
+ setPreset: (presetId) => commit({ ...selection, presetId }),
72
+ setMask: (mask) => commit({ ...selection, mask }),
73
+ setPatch: (presetId, patch) => commit({ ...selection, patches: (0, state_1.mergePatch)(selection.patches, presetId, patch) }),
74
+ // The stored wire shape is book-agnostic; the typed view is recovered at the
75
+ // hook (`useKaleidoscopeState<typeof presets>()`), so the cast is the seam.
76
+ patchesFor: ((presetId) => (0, state_1.patchListFor)(selection.patches, presetId)),
77
+ reset: () => commit({ presetId: null, mask: defaultMask, patches: {} }),
78
+ };
79
+ return ((0, jsx_runtime_1.jsx)(KaleidoscopeStateContext.Provider, { value: value, children: children }));
80
+ }
81
+ /**
82
+ * The persisted selection plus its setters. Typed by the consumer's book:
83
+ * `useKaleidoscopeState<typeof presets>()`. Throws outside the provider.
84
+ */
85
+ function useKaleidoscopeState() {
86
+ const value = (0, react_1.useContext)(KaleidoscopeStateContext);
87
+ if (value === null) {
88
+ throw new Error('useKaleidoscopeState: no <KaleidoscopeStateProvider> above this component. ' +
89
+ 'Wrap your app (or the screen using the picker) in the provider from ' +
90
+ "'react-native-webrtc-kaleidoscope/persistence'.");
91
+ }
92
+ // Safe by construction: the provider pruned ids against the same book the
93
+ // consumer parameterizes with; the runtime shapes are identical.
94
+ return value;
95
+ }
96
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../src/persistence/provider.tsx"],"names":[],"mappings":"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","sourcesContent":["// Persistence: the React surface. `KaleidoscopeStateProvider` hydrates the\n// stored selection once at mount and writes through on every change;\n// `useKaleidoscopeState` hands the host the hydrated values plus the setters.\n//\n// The provider owns STORAGE state only; it never binds a track or calls the\n// verbs. The host applies the restored selection itself (gated on `hydrated`,\n// so a stored preset is not flashed over by the default):\n//\n// const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();\n// useEffect(() => {\n// if (!hydrated || !controls) return;\n// if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));\n// else controls.kaleidoscope(null);\n// }, [hydrated, controls, presetId]);\n//\n// The backing store defaults to AsyncStorage; pass any `KaleidoscopeStateStore`\n// to swap it. Importing this subpath is what brings the optional\n// `@react-native-async-storage/async-storage` peer onto your bundle path\n// (Metro resolves it at bundle time either way, so there is no lazy escape).\n\nimport {\n createContext,\n type ReactElement,\n type ReactNode,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport type { MaskInput, PatchesFor } from '../kaleidoscope/types';\nimport type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';\nimport { kaleidoscopeAsyncStorageStore } from './async-storage-store';\nimport {\n DEFAULT_MASK,\n type KaleidoscopeStateStore,\n mergePatch,\n patchListFor,\n pruneStoredState,\n type StoredPatch,\n type StoredPatches,\n} from './state';\n\nexport type KaleidoscopeStateValue<P extends KaleidoscopePresetBook = KaleidoscopePresetBook> = {\n /** False until the persisted selection has been read; apply no effects before then. */\n readonly hydrated: boolean;\n /** The selected preset id, or null when nothing is selected. */\n readonly presetId: (keyof P & string) | null;\n /** The shared segmentation edge. */\n readonly mask: MaskInput;\n /** Every preset's stored per-layer overrides (keyed by preset id, then layer id). */\n readonly patches: StoredPatches;\n readonly setPreset: (presetId: (keyof P & string) | null) => void;\n readonly setMask: (mask: MaskInput) => void;\n /** Record one control-panel patch against a preset (and persist it). */\n readonly setPatch: (presetId: keyof P & string, patch: StoredPatch) => void;\n /** A preset's stored overrides in the array shape `kaleidoscope(id, patches)` takes. */\n readonly patchesFor: <K extends keyof P & string>(presetId: K) => PatchesFor<P, K>;\n /** Clear the stored selection back to defaults (and persist the cleared state). */\n readonly reset: () => void;\n};\n\nconst KaleidoscopeStateContext = createContext<KaleidoscopeStateValue | null>(null);\n\nexport type KaleidoscopeStateProviderProps<P extends KaleidoscopePresetBook> = {\n /** The consumer's preset book; stored state is pruned against it at hydrate. */\n readonly presets: P;\n /** The backing store. Defaults to the AsyncStorage store (lazily loaded). */\n readonly store?: KaleidoscopeStateStore;\n /** The mask used before hydration and after `reset`. Defaults to 0.5/0.5. */\n readonly defaultMask?: MaskInput;\n readonly children: ReactNode;\n};\n\ntype Selection = {\n readonly presetId: string | null;\n readonly mask: MaskInput;\n readonly patches: StoredPatches;\n};\n\nexport function KaleidoscopeStateProvider<P extends KaleidoscopePresetBook>({\n presets,\n store,\n defaultMask = DEFAULT_MASK,\n children,\n}: KaleidoscopeStateProviderProps<P>): ReactElement {\n const [hydrated, setHydrated] = useState(false);\n const [selection, setSelection] = useState<Selection>({\n presetId: null,\n mask: defaultMask,\n patches: {},\n });\n // A write before hydration wins over the stored value (the person acted; do\n // not clobber their fresh choice with yesterday's).\n const dirty = useRef(false);\n // Pinned for the provider's lifetime: hydrate and every write-through go to\n // the same store the first render saw.\n const storeRef = useRef(store ?? kaleidoscopeAsyncStorageStore);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: hydrate exactly once; the book and store are bind-time constants.\n useEffect(() => {\n let cancelled = false;\n storeRef.current.load().then(\n (stored) => {\n if (cancelled) return;\n if (stored && !dirty.current) {\n const pruned = pruneStoredState(stored, presets);\n setSelection({ presetId: pruned.presetId, mask: pruned.mask, patches: pruned.patches });\n }\n setHydrated(true);\n },\n () => {\n if (!cancelled) setHydrated(true);\n },\n );\n return () => {\n cancelled = true;\n };\n }, []);\n\n const commit = (next: Selection): void => {\n dirty.current = true;\n setSelection(next);\n void storeRef.current.save({ version: 1, ...next });\n };\n\n const value: KaleidoscopeStateValue = {\n hydrated,\n presetId: selection.presetId,\n mask: selection.mask,\n patches: selection.patches,\n setPreset: (presetId) => commit({ ...selection, presetId }),\n setMask: (mask) => commit({ ...selection, mask }),\n setPatch: (presetId, patch) =>\n commit({ ...selection, patches: mergePatch(selection.patches, presetId, patch) }),\n // The stored wire shape is book-agnostic; the typed view is recovered at the\n // hook (`useKaleidoscopeState<typeof presets>()`), so the cast is the seam.\n patchesFor: ((presetId: string) =>\n patchListFor(selection.patches, presetId)) as KaleidoscopeStateValue['patchesFor'],\n reset: () => commit({ presetId: null, mask: defaultMask, patches: {} }),\n };\n\n return (\n <KaleidoscopeStateContext.Provider value={value}>{children}</KaleidoscopeStateContext.Provider>\n );\n}\n\n/**\n * The persisted selection plus its setters. Typed by the consumer's book:\n * `useKaleidoscopeState<typeof presets>()`. Throws outside the provider.\n */\nexport function useKaleidoscopeState<\n P extends KaleidoscopePresetBook = KaleidoscopePresetBook,\n>(): KaleidoscopeStateValue<P> {\n const value = useContext(KaleidoscopeStateContext);\n if (value === null) {\n throw new Error(\n 'useKaleidoscopeState: no <KaleidoscopeStateProvider> above this component. ' +\n 'Wrap your app (or the screen using the picker) in the provider from ' +\n \"'react-native-webrtc-kaleidoscope/persistence'.\",\n );\n }\n // Safe by construction: the provider pruned ids against the same book the\n // consumer parameterizes with; the runtime shapes are identical.\n return value as unknown as KaleidoscopeStateValue<P>;\n}\n"]}
@@ -0,0 +1,54 @@
1
+ import type { MaskInput } from '../kaleidoscope/types';
2
+ import type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';
3
+ /** One layer's stored uniform overrides (the wire shape `onPatch` emits). */
4
+ export type StoredLayerUniforms = Readonly<Record<string, number | readonly number[]>>;
5
+ /** A preset's stored overrides, keyed by layer id. */
6
+ export type StoredPatchMap = Readonly<Record<string, StoredLayerUniforms>>;
7
+ /** Every preset's stored overrides, keyed by preset id. */
8
+ export type StoredPatches = Readonly<Record<string, StoredPatchMap>>;
9
+ /** The single live-patch shape, identical to `KaleidoscopeControls['onPatch']`'s argument. */
10
+ export type StoredPatch = {
11
+ readonly id: string;
12
+ readonly uniforms: StoredLayerUniforms;
13
+ };
14
+ export type StoredKaleidoscopeState = {
15
+ readonly version: 1;
16
+ /** The last-applied preset id, or null when nothing was selected. */
17
+ readonly presetId: string | null;
18
+ /** The shared segmentation edge. */
19
+ readonly mask: MaskInput;
20
+ /** Per-preset, per-layer uniform overrides from the control panels. */
21
+ readonly patches: StoredPatches;
22
+ };
23
+ /**
24
+ * The backing store. `load` resolves null when nothing (or nothing readable) is
25
+ * stored; `save` swallows its own failures (persistence is a convenience, never
26
+ * a crash).
27
+ */
28
+ export interface KaleidoscopeStateStore {
29
+ load(): Promise<StoredKaleidoscopeState | null>;
30
+ save(state: StoredKaleidoscopeState): Promise<void>;
31
+ }
32
+ export declare const KALEIDOSCOPE_STATE_KEY = "kaleidoscope.state.v1";
33
+ export declare const DEFAULT_MASK: MaskInput;
34
+ export declare const serializeKaleidoscopeState: (state: StoredKaleidoscopeState) => string;
35
+ /**
36
+ * Tolerant parse: any malformed or wrong-version payload reads as null, and a
37
+ * malformed `patches` subtree degrades to the valid subset rather than killing
38
+ * the whole state. Mask values clamp to 0..1 (the verbs' documented range).
39
+ */
40
+ export declare const parseStoredKaleidoscopeState: (raw: string | null) => StoredKaleidoscopeState | null;
41
+ /**
42
+ * Reconcile stored state against the consumer's current book: a preset that no
43
+ * longer exists reads as "none", a patch for a vanished preset or layer is
44
+ * dropped. Stale state degrades silently; it never crashes the picker.
45
+ */
46
+ export declare const pruneStoredState: (state: StoredKaleidoscopeState, book: KaleidoscopePresetBook) => StoredKaleidoscopeState;
47
+ /** Merge one live patch into a preset's stored overrides (immutably). */
48
+ export declare const mergePatch: (patches: StoredPatches, presetId: string, patch: StoredPatch) => StoredPatches;
49
+ /**
50
+ * Project a preset's stored overrides into the array shape the `kaleidoscope`
51
+ * verb takes as `patches`.
52
+ */
53
+ export declare const patchListFor: (patches: StoredPatches, presetId: string) => ReadonlyArray<StoredPatch>;
54
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/persistence/state.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAEhF,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;AAEvF,sDAAsD;AACtD,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAE3E,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;AAErE,8FAA8F;AAC9F,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;CACjC,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,IAAI,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IAChD,IAAI,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED,eAAO,MAAM,sBAAsB,0BAA0B,CAAC;AAE9D,eAAO,MAAM,YAAY,EAAE,SAA6C,CAAC;AAEzE,eAAO,MAAM,0BAA0B,UAAW,uBAAuB,KAAG,MACrD,CAAC;AAuBxB;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,QAClC,MAAM,GAAG,IAAI,KACjB,uBAAuB,GAAG,IA6B5B,CAAC;AAUF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,UACpB,uBAAuB,QACxB,sBAAsB,KAC3B,uBAaF,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,UAAU,YACZ,aAAa,YACZ,MAAM,SACT,WAAW,KACjB,aAMD,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,YACd,aAAa,YACZ,MAAM,KACf,aAAa,CAAC,WAAW,CACyD,CAAC"}
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ // Persistence: the pure state module. The stored shape, its key, the tolerant
3
+ // parse, and the pure helpers the provider uses to prune, merge, and project
4
+ // stored state. No React, no storage; the store interface is the only boundary
5
+ // (a consumer may back it with AsyncStorage, localStorage, MMKV, anything
6
+ // promise-shaped). Everything here is unit-testable in plain Node.
7
+ //
8
+ // What persists is the person's selection: the preset id they last applied, the
9
+ // per-layer uniform patches they dialed in through the control panels (keyed by
10
+ // preset, so tweaks to several presets all survive), and the shared mask edge.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.patchListFor = exports.mergePatch = exports.pruneStoredState = exports.parseStoredKaleidoscopeState = exports.serializeKaleidoscopeState = exports.DEFAULT_MASK = exports.KALEIDOSCOPE_STATE_KEY = void 0;
13
+ exports.KALEIDOSCOPE_STATE_KEY = 'kaleidoscope.state.v1';
14
+ exports.DEFAULT_MASK = { hardness: 0.5, threshold: 0.5 };
15
+ const serializeKaleidoscopeState = (state) => JSON.stringify(state);
16
+ exports.serializeKaleidoscopeState = serializeKaleidoscopeState;
17
+ const clamp01 = (value) => Math.min(1, Math.max(0, value));
18
+ const isUniformValue = (value) => typeof value === 'number' ||
19
+ (Array.isArray(value) && value.every((entry) => typeof entry === 'number'));
20
+ // A patch map (layer id -> uniforms) with every malformed entry dropped.
21
+ const parsePatchMap = (raw) => {
22
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
23
+ return null;
24
+ const out = {};
25
+ for (const [layerId, uniforms] of Object.entries(raw)) {
26
+ if (typeof uniforms !== 'object' || uniforms === null || Array.isArray(uniforms))
27
+ continue;
28
+ const kept = {};
29
+ for (const [key, value] of Object.entries(uniforms)) {
30
+ if (isUniformValue(value))
31
+ kept[key] = value;
32
+ }
33
+ if (Object.keys(kept).length > 0)
34
+ out[layerId] = kept;
35
+ }
36
+ return out;
37
+ };
38
+ /**
39
+ * Tolerant parse: any malformed or wrong-version payload reads as null, and a
40
+ * malformed `patches` subtree degrades to the valid subset rather than killing
41
+ * the whole state. Mask values clamp to 0..1 (the verbs' documented range).
42
+ */
43
+ const parseStoredKaleidoscopeState = (raw) => {
44
+ if (!raw)
45
+ return null;
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (typeof parsed !== 'object' || parsed === null)
54
+ return null;
55
+ const candidate = parsed;
56
+ if (candidate.version !== 1)
57
+ return null;
58
+ if (candidate.presetId !== null && typeof candidate.presetId !== 'string')
59
+ return null;
60
+ const mask = candidate.mask;
61
+ if (!mask || typeof mask.hardness !== 'number' || typeof mask.threshold !== 'number') {
62
+ return null;
63
+ }
64
+ const patches = {};
65
+ if (typeof candidate.patches === 'object' && candidate.patches !== null) {
66
+ for (const [presetId, rawMap] of Object.entries(candidate.patches)) {
67
+ const map = parsePatchMap(rawMap);
68
+ if (map && Object.keys(map).length > 0)
69
+ patches[presetId] = map;
70
+ }
71
+ }
72
+ return {
73
+ version: 1,
74
+ presetId: candidate.presetId,
75
+ mask: { hardness: clamp01(mask.hardness), threshold: clamp01(mask.threshold) },
76
+ patches,
77
+ };
78
+ };
79
+ exports.parseStoredKaleidoscopeState = parseStoredKaleidoscopeState;
80
+ // The layer ids a preset can be patched on: the layers that carry uniforms
81
+ // (`image` and `direct` layers have none and cannot be patched).
82
+ const tunableLayerIds = (book, presetId) => {
83
+ const preset = book[presetId];
84
+ if (!preset)
85
+ return new Set();
86
+ return new Set(preset.layers.filter((layer) => 'uniforms' in layer).map((layer) => layer.id));
87
+ };
88
+ /**
89
+ * Reconcile stored state against the consumer's current book: a preset that no
90
+ * longer exists reads as "none", a patch for a vanished preset or layer is
91
+ * dropped. Stale state degrades silently; it never crashes the picker.
92
+ */
93
+ const pruneStoredState = (state, book) => {
94
+ const presetId = state.presetId !== null && state.presetId in book ? state.presetId : null;
95
+ const patches = {};
96
+ for (const [id, map] of Object.entries(state.patches)) {
97
+ if (!(id in book))
98
+ continue;
99
+ const tunable = tunableLayerIds(book, id);
100
+ const kept = {};
101
+ for (const [layerId, uniforms] of Object.entries(map)) {
102
+ if (tunable.has(layerId))
103
+ kept[layerId] = uniforms;
104
+ }
105
+ if (Object.keys(kept).length > 0)
106
+ patches[id] = kept;
107
+ }
108
+ return { version: 1, presetId, mask: state.mask, patches };
109
+ };
110
+ exports.pruneStoredState = pruneStoredState;
111
+ /** Merge one live patch into a preset's stored overrides (immutably). */
112
+ const mergePatch = (patches, presetId, patch) => ({
113
+ ...patches,
114
+ [presetId]: {
115
+ ...patches[presetId],
116
+ [patch.id]: { ...patches[presetId]?.[patch.id], ...patch.uniforms },
117
+ },
118
+ });
119
+ exports.mergePatch = mergePatch;
120
+ /**
121
+ * Project a preset's stored overrides into the array shape the `kaleidoscope`
122
+ * verb takes as `patches`.
123
+ */
124
+ const patchListFor = (patches, presetId) => Object.entries(patches[presetId] ?? {}).map(([id, uniforms]) => ({ id, uniforms }));
125
+ exports.patchListFor = patchListFor;
126
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/persistence/state.ts"],"names":[],"mappings":"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","sourcesContent":["// Persistence: the pure state module. The stored shape, its key, the tolerant\n// parse, and the pure helpers the provider uses to prune, merge, and project\n// stored state. No React, no storage; the store interface is the only boundary\n// (a consumer may back it with AsyncStorage, localStorage, MMKV, anything\n// promise-shaped). Everything here is unit-testable in plain Node.\n//\n// What persists is the person's selection: the preset id they last applied, the\n// per-layer uniform patches they dialed in through the control panels (keyed by\n// preset, so tweaks to several presets all survive), and the shared mask edge.\n\nimport type { MaskInput } from '../kaleidoscope/types';\nimport type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';\n\n/** One layer's stored uniform overrides (the wire shape `onPatch` emits). */\nexport type StoredLayerUniforms = Readonly<Record<string, number | readonly number[]>>;\n\n/** A preset's stored overrides, keyed by layer id. */\nexport type StoredPatchMap = Readonly<Record<string, StoredLayerUniforms>>;\n\n/** Every preset's stored overrides, keyed by preset id. */\nexport type StoredPatches = Readonly<Record<string, StoredPatchMap>>;\n\n/** The single live-patch shape, identical to `KaleidoscopeControls['onPatch']`'s argument. */\nexport type StoredPatch = {\n readonly id: string;\n readonly uniforms: StoredLayerUniforms;\n};\n\nexport type StoredKaleidoscopeState = {\n readonly version: 1;\n /** The last-applied preset id, or null when nothing was selected. */\n readonly presetId: string | null;\n /** The shared segmentation edge. */\n readonly mask: MaskInput;\n /** Per-preset, per-layer uniform overrides from the control panels. */\n readonly patches: StoredPatches;\n};\n\n/**\n * The backing store. `load` resolves null when nothing (or nothing readable) is\n * stored; `save` swallows its own failures (persistence is a convenience, never\n * a crash).\n */\nexport interface KaleidoscopeStateStore {\n load(): Promise<StoredKaleidoscopeState | null>;\n save(state: StoredKaleidoscopeState): Promise<void>;\n}\n\nexport const KALEIDOSCOPE_STATE_KEY = 'kaleidoscope.state.v1';\n\nexport const DEFAULT_MASK: MaskInput = { hardness: 0.5, threshold: 0.5 };\n\nexport const serializeKaleidoscopeState = (state: StoredKaleidoscopeState): string =>\n JSON.stringify(state);\n\nconst clamp01 = (value: number): number => Math.min(1, Math.max(0, value));\n\nconst isUniformValue = (value: unknown): value is number | readonly number[] =>\n typeof value === 'number' ||\n (Array.isArray(value) && value.every((entry) => typeof entry === 'number'));\n\n// A patch map (layer id -> uniforms) with every malformed entry dropped.\nconst parsePatchMap = (raw: unknown): StoredPatchMap | null => {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return null;\n const out: Record<string, StoredLayerUniforms> = {};\n for (const [layerId, uniforms] of Object.entries(raw)) {\n if (typeof uniforms !== 'object' || uniforms === null || Array.isArray(uniforms)) continue;\n const kept: Record<string, number | readonly number[]> = {};\n for (const [key, value] of Object.entries(uniforms)) {\n if (isUniformValue(value)) kept[key] = value;\n }\n if (Object.keys(kept).length > 0) out[layerId] = kept;\n }\n return out;\n};\n\n/**\n * Tolerant parse: any malformed or wrong-version payload reads as null, and a\n * malformed `patches` subtree degrades to the valid subset rather than killing\n * the whole state. Mask values clamp to 0..1 (the verbs' documented range).\n */\nexport const parseStoredKaleidoscopeState = (\n raw: string | null,\n): StoredKaleidoscopeState | null => {\n if (!raw) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (typeof parsed !== 'object' || parsed === null) return null;\n const candidate = parsed as Record<string, unknown>;\n if (candidate.version !== 1) return null;\n if (candidate.presetId !== null && typeof candidate.presetId !== 'string') return null;\n const mask = candidate.mask as Record<string, unknown> | null | undefined;\n if (!mask || typeof mask.hardness !== 'number' || typeof mask.threshold !== 'number') {\n return null;\n }\n const patches: Record<string, StoredPatchMap> = {};\n if (typeof candidate.patches === 'object' && candidate.patches !== null) {\n for (const [presetId, rawMap] of Object.entries(candidate.patches)) {\n const map = parsePatchMap(rawMap);\n if (map && Object.keys(map).length > 0) patches[presetId] = map;\n }\n }\n return {\n version: 1,\n presetId: candidate.presetId as string | null,\n mask: { hardness: clamp01(mask.hardness), threshold: clamp01(mask.threshold) },\n patches,\n };\n};\n\n// The layer ids a preset can be patched on: the layers that carry uniforms\n// (`image` and `direct` layers have none and cannot be patched).\nconst tunableLayerIds = (book: KaleidoscopePresetBook, presetId: string): ReadonlySet<string> => {\n const preset = book[presetId];\n if (!preset) return new Set();\n return new Set(preset.layers.filter((layer) => 'uniforms' in layer).map((layer) => layer.id));\n};\n\n/**\n * Reconcile stored state against the consumer's current book: a preset that no\n * longer exists reads as \"none\", a patch for a vanished preset or layer is\n * dropped. Stale state degrades silently; it never crashes the picker.\n */\nexport const pruneStoredState = (\n state: StoredKaleidoscopeState,\n book: KaleidoscopePresetBook,\n): StoredKaleidoscopeState => {\n const presetId = state.presetId !== null && state.presetId in book ? state.presetId : null;\n const patches: Record<string, StoredPatchMap> = {};\n for (const [id, map] of Object.entries(state.patches)) {\n if (!(id in book)) continue;\n const tunable = tunableLayerIds(book, id);\n const kept: Record<string, StoredLayerUniforms> = {};\n for (const [layerId, uniforms] of Object.entries(map)) {\n if (tunable.has(layerId)) kept[layerId] = uniforms;\n }\n if (Object.keys(kept).length > 0) patches[id] = kept;\n }\n return { version: 1, presetId, mask: state.mask, patches };\n};\n\n/** Merge one live patch into a preset's stored overrides (immutably). */\nexport const mergePatch = (\n patches: StoredPatches,\n presetId: string,\n patch: StoredPatch,\n): StoredPatches => ({\n ...patches,\n [presetId]: {\n ...patches[presetId],\n [patch.id]: { ...patches[presetId]?.[patch.id], ...patch.uniforms },\n },\n});\n\n/**\n * Project a preset's stored overrides into the array shape the `kaleidoscope`\n * verb takes as `patches`.\n */\nexport const patchListFor = (\n patches: StoredPatches,\n presetId: string,\n): ReadonlyArray<StoredPatch> =>\n Object.entries(patches[presetId] ?? {}).map(([id, uniforms]) => ({ id, uniforms }));\n"]}
@@ -5,11 +5,11 @@ export declare const COMPOSITE_IMAGE_FRAG_SRC = "#version 300 es\nprecision high
5
5
  export declare const COMPOSITE_SUBJECT_FRAG_SRC = "#version 300 es\nprecision highp float;\nuniform sampler2D uCamera;\nuniform sampler2D uMask;\nuniform vec2 uMaskUvScale;\nuniform vec2 uMaskUvOffset;\nuniform float uMaskLo;\nuniform float uMaskHi;\nin highp vec2 vUv;\nout vec4 oColor;\nvoid main() {\n vec3 cam = texture(uCamera, vUv).rgb;\n float raw = texture(uMask, vUv * uMaskUvScale + uMaskUvOffset).r;\n float safeHi = max(uMaskHi, uMaskLo + 0.001);\n float a = clamp(smoothstep(uMaskLo, safeHi, raw), 0.0, 1.0);\n oColor = vec4(cam * a, a);\n}\n";
6
6
  export declare const COMPOSITE_MASKED_FRAG_SRC = "#version 300 es\nprecision highp float;\nuniform sampler2D uTex;\nuniform sampler2D uMask;\nuniform vec2 uMaskUvScale;\nuniform vec2 uMaskUvOffset;\nuniform float uMaskLo;\nuniform float uMaskHi;\nin highp vec2 vUv;\nout vec4 oColor;\nvoid main() {\n vec4 c = texture(uTex, vUv);\n float raw = texture(uMask, vUv * uMaskUvScale + uMaskUvOffset).r;\n float safeHi = max(uMaskHi, uMaskLo + 0.001);\n float a = clamp(smoothstep(uMaskLo, safeHi, raw), 0.0, 1.0);\n oColor = c * a;\n}\n";
7
7
  export declare const PLASMA_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform vec2 uResolution; // framebuffer size in pixels; both components > 0\nuniform vec3 uColorA; // first palette color, linear-ish RGB in [0, 1]\nuniform vec3 uColorB; // second palette color, linear-ish RGB in [0, 1]\nuniform float uSpeed; // animation rate multiplier; 0 freezes the field\nuniform float uScale; // spatial frequency; higher = more, tighter cells\n\nin highp vec2 vUv;\nout vec4 oColor;\n\nvoid main() {\n // Aspect-correct, screen-centered coordinates (matches nebula.frag): divide\n // by the height so uScale reads the same regardless of aspect ratio.\n vec2 fragCoord = vUv * uResolution;\n vec2 uv = (fragCoord - 0.5 * uResolution) / uResolution.y;\n\n float t = uTime * uSpeed;\n\n // Classic demoscene plasma: a few sines of position and time. The radial\n // term (length(uv)) gives the field an organic, non-grid-aligned drift.\n float v = sin(uv.x * uScale + t);\n v += sin(uv.y * uScale + t * 0.8);\n v += sin((uv.x + uv.y) * uScale * 0.7 + t * 1.3);\n v += sin(length(uv) * uScale * 1.2 - t);\n\n // v ranges roughly [-4, 4]; fold through sin to a smooth [0, 1] mix factor.\n float mixT = 0.5 + 0.5 * sin(v);\n\n // Opaque procedural background; the person is composited over it downstream.\n oColor = vec4(mix(uColorA, uColorB, mixT), 1.0);\n}\n";
8
- export declare const CLOUDS_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2 uResolution;\nuniform vec3 uSkyLowColor;\nuniform vec3 uSkyHighColor;\nuniform vec3 uCloudLightColor;\nuniform vec3 uCloudDarkColor;\nuniform float uExposure;\nuniform float uStepSize;\nuniform float uCloudSpeed;\nuniform float uCloudScale;\nuniform float uDensity;\nuniform float uCoverage;\nuniform float uSoftness;\n\nin highp vec2 vUv;\nout vec4 oColor;\n\n// STEPS must stay a compile-time constant (GLSL ES loop bound).\n#define STEPS 48\n\nfloat hash(vec3 p) {\n p = fract(p * 0.3183099 + 0.1);\n p *= 17.0;\n return fract(p.x * p.y * p.z * (p.x + p.y + p.z));\n}\n\nfloat rand(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat noise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(\n mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x),\n mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x),\n f.y),\n mix(\n mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),\n mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x),\n f.y),\n f.z);\n}\n\nfloat fbm(vec3 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 5; i++) {\n v += a * noise(p);\n p *= 2.03;\n a *= 0.5;\n }\n return v;\n}\n\nfloat cloudDensity(vec3 p) {\n p += vec3(uTime * uCloudSpeed, 0.0, uTime * uCloudSpeed * 0.35);\n // Outside the slab the height mask is 0, so the sample is 0; bail before fbm.\n if (p.y <= 0.0 || p.y >= 3.0) return 0.0;\n float n = fbm(p * uCloudScale);\n float bottom = smoothstep(0.0, 0.7, p.y);\n float top = smoothstep(3.0, 1.2, p.y);\n float heightMask = bottom * top;\n float cloud = smoothstep(uCoverage, uCoverage + uSoftness, n);\n return cloud * heightMask;\n}\n\nvoid main() {\n vec2 fragCoord = vUv * uResolution;\n vec2 uv = (fragCoord - 0.5 * uResolution) / uResolution.y;\n vec3 ro = vec3(0.0, 1.2, -4.0);\n vec3 rd = normalize(vec3(uv, 1.5));\n float skyGradient = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);\n vec3 skyColor = mix(uSkyLowColor, uSkyHighColor, skyGradient);\n vec3 accum = vec3(0.0);\n float alpha = 0.0;\n float t = rand(fragCoord) * uStepSize;\n for (int i = 0; i < STEPS; i++) {\n vec3 p = ro + rd * t;\n // The slab is crossed monotonically in t; once past it, all samples are 0.\n if (rd.y > 0.0 && p.y >= 3.0) break;\n if (rd.y < 0.0 && p.y <= 0.0) break;\n float d = cloudDensity(p);\n if (d > 0.01) {\n float light = smoothstep(0.4, 2.8, p.y);\n vec3 sampleColor = mix(uCloudDarkColor, uCloudLightColor, light);\n float a = d * uDensity;\n accum += (1.0 - alpha) * sampleColor * a;\n alpha += (1.0 - alpha) * a;\n }\n t += uStepSize;\n if (alpha > 0.95) break;\n }\n vec3 color = mix(skyColor, accum, alpha);\n color *= uExposure;\n color = pow(color, vec3(0.9));\n oColor = vec4(color, 1.0);\n}\n";
9
- export declare const NEBULA_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform vec2 uResolution; // framebuffer size in pixels; both components > 0\nuniform vec3 uColor; // overall tint / color grade; [1,1,1] = untinted\nuniform float uBrightness; // final glow multiplier; 1.0 = stock\nuniform float uSpeed; // drift + rotation rate; 1.0 = stock, 0 freezes\nuniform float uTwinkleSpeed; // star color-cycle rate; 1.0 = stock\nuniform float uScale; // starfield zoom / density; >1 = more, smaller stars\nuniform float uStarGlow; // star-core size; 1.0 = stock\n\nin highp vec2 vUv;\nout vec4 oColor;\n\nconst float PI = 3.14159265;\nconst float MIN_DIVIDE = 64.0;\nconst float MAX_DIVIDE = 0.01;\n// Number of stacked starfield layers. Compile-time constant so the layer\n// loop has a fixed integer bound (cross-compile-safe; no float loop counter).\nconst int STARFIELD_LAYERS_COUNT = 12;\n\nmat2 Rotate(float angle) {\n float s = sin(angle);\n float c = cos(angle);\n return mat2(c, -s, s, c);\n}\n\nfloat Star(vec2 uv, float flaresize, float rotAngle, float randomN) {\n float d = length(uv);\n // Star core. Guard the division: length(uv) can be exactly 0 at a cell\n // center, which yields inf/NaN under Metal. max(d, 1e-4) caps the core\n // brightness without visibly changing the look (the concentric\n // smoothstep fade below already clamps it).\n float starcore = 0.05 * uStarGlow / max(d, 1e-4);\n uv *= Rotate(-2.0 * PI * rotAngle);\n float flareMax = 1.0;\n\n // flares\n float starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * flaresize;\n uv *= Rotate(PI * 0.25);\n starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * 0.3 * flaresize;\n // light can't go forever, fade it concentrically.\n starcore *= smoothstep(1.0, 0.05, d);\n return starcore;\n}\n\nfloat PseudoRandomizer(vec2 p) {\n // not really random, but it looks random.\n p = fract(p * vec2(123.45, 345.67));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nvec3 StarFieldLayer(vec2 uv, float rotAngle) {\n vec3 col = vec3(0.0);\n\n vec2 gv = fract(uv) - 0.5;\n vec2 id = floor(uv);\n\n float deltaTimeTwinkle = uTime * 0.35 * uTwinkleSpeed;\n\n // sweep the 8 neighbors plus the home cell so stars are not clipped at\n // cell borders. Constant 3x3 bounds.\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n vec2 offset = vec2(float(x), float(y));\n\n float randomN = PseudoRandomizer(id + offset); // 0..1\n float randoX = randomN - 0.5;\n float randoY = fract(randomN * 45.0) - 0.5;\n vec2 randomPosition = gv - offset - vec2(randoX, randoY);\n // fract trick: random sizes\n float size = fract(randomN * 1356.33);\n float flareSwitch = smoothstep(0.9, 1.0, size);\n float star = Star(randomPosition, flareSwitch, rotAngle, randomN);\n\n // fract trick: random colors\n float randomStarColorSeed = fract(randomN * 2150.0) * (3.0 * PI) * deltaTimeTwinkle;\n vec3 color = sin(vec3(0.7, 0.3, 0.9) * randomStarColorSeed);\n\n // compress\n color = color * (0.4 * sin(deltaTimeTwinkle)) + 0.6;\n // filter\n color = color * vec3(1.0, 0.1, 0.9 + size);\n float dimByDensity = 15.0 / float(STARFIELD_LAYERS_COUNT);\n col += star * size * color * dimByDensity;\n }\n }\n\n return col;\n}\n\nvoid main() {\n // ShaderToy fragCoord, reconstructed from vUv (see header).\n vec2 fragCoord = vUv * uResolution;\n\n // Normalized pixel coordinates centered at screen middle.\n vec2 uv = (fragCoord - 0.5 * uResolution.xy) / uResolution.y;\n\n float deltaTime = uTime * 0.01 * uSpeed;\n\n vec3 col = vec3(0.0);\n\n float rotAngle = deltaTime * 0.09;\n\n // Layer accumulation. Integer-counted loop replacing the original\n // `for (float i = 0.0; i < 1.0; i += 1.0/COUNT)`. With n in [0, COUNT),\n // i = n/COUNT reproduces the exact same {0, 1/N, 2/N, ...} sequence and\n // the same iteration count, so visual output is unchanged; only the loop\n // form is cross-compile-safe.\n for (int n = 0; n < STARFIELD_LAYERS_COUNT; n++) {\n float i = float(n) / float(STARFIELD_LAYERS_COUNT);\n float layerDepth = fract(i + deltaTime);\n float layerScale = mix(MIN_DIVIDE, MAX_DIVIDE, layerDepth);\n float layerFader = layerDepth * smoothstep(0.1, 1.1, layerDepth);\n float layerOffset = i * (3430.0 + fract(i));\n mat2 layerRot = Rotate(rotAngle * i * -10.0);\n uv *= layerRot;\n vec2 starfieldUv = uv * layerScale * uScale + layerOffset;\n col += StarFieldLayer(starfieldUv, rotAngle) * layerFader;\n }\n\n // Glow + color grade, then opaque procedural background.\n col *= uBrightness * uColor;\n oColor = vec4(col, 1.0);\n}\n";
8
+ export declare const CLOUDS_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2 uResolution;\nuniform vec3 uSkyLowColor;\nuniform vec3 uSkyHighColor;\nuniform vec3 uCloudLightColor;\nuniform vec3 uCloudDarkColor;\nuniform float uExposure;\nuniform float uStepSize;\nuniform float uCloudSpeed;\nuniform float uCloudScale;\nuniform float uDensity;\nuniform float uCoverage;\nuniform float uSoftness;\n\nin highp vec2 vUv;\nout vec4 oColor;\n\n// STEPS must stay a compile-time constant (GLSL ES loop bound).\n// 32 (was 48; issue #37): the distance-growing step in main() keeps the\n// marched range, so fewer steps buys speed instead of clipping the horizon.\n#define STEPS 32\n\nfloat hash(vec3 p) {\n p = fract(p * 0.3183099 + 0.1);\n p *= 17.0;\n return fract(p.x * p.y * p.z * (p.x + p.y + p.z));\n}\n\nfloat rand(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat noise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(\n mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x),\n mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x),\n f.y),\n mix(\n mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),\n mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x),\n f.y),\n f.z);\n}\n\n// 4 octaves (was 5; issue #37): the 5th octave is fine wisp detail the\n// smoothstep(uCoverage, uCoverage + uSoftness, n) threshold mostly eats; each\n// octave is 8 hash() calls per sample, so this is a flat -20% on the march.\nfloat fbm(vec3 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 4; i++) {\n v += a * noise(p);\n p *= 2.03;\n a *= 0.5;\n }\n return v;\n}\n\nfloat cloudDensity(vec3 p) {\n p += vec3(uTime * uCloudSpeed, 0.0, uTime * uCloudSpeed * 0.35);\n // Outside the slab the height mask is 0, so the sample is 0; bail before fbm.\n if (p.y <= 0.0 || p.y >= 3.0) return 0.0;\n float n = fbm(p * uCloudScale);\n float bottom = smoothstep(0.0, 0.7, p.y);\n float top = smoothstep(3.0, 1.2, p.y);\n float heightMask = bottom * top;\n float cloud = smoothstep(uCoverage, uCoverage + uSoftness, n);\n return cloud * heightMask;\n}\n\nvoid main() {\n vec2 fragCoord = vUv * uResolution;\n vec2 uv = (fragCoord - 0.5 * uResolution) / uResolution.y;\n vec3 ro = vec3(0.0, 1.2, -4.0);\n vec3 rd = normalize(vec3(uv, 1.5));\n float skyGradient = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);\n vec3 skyColor = mix(uSkyLowColor, uSkyHighColor, skyGradient);\n vec3 accum = vec3(0.0);\n float alpha = 0.0;\n float t = rand(fragCoord) * uStepSize;\n for (int i = 0; i < STEPS; i++) {\n vec3 p = ro + rd * t;\n // The slab is crossed monotonically in t; once past it, all samples are 0.\n if (rd.y > 0.0 && p.y >= 3.0) break;\n if (rd.y < 0.0 && p.y <= 0.0) break;\n float d = cloudDensity(p);\n // Distance-growing step (issue #37): far clouds are small on screen and\n // tolerate coarser sampling, so the step stretches with t. 32 growing\n // steps reach slightly past where 48 uniform steps did, spending the\n // samples up close where banding would show. growth also scales the\n // per-sample opacity so optical depth per unit distance stays consistent\n // with the uniform-step tuning the presets were dialed against.\n float growth = 1.0 + t * 0.15;\n if (d > 0.01) {\n float light = smoothstep(0.4, 2.8, p.y);\n vec3 sampleColor = mix(uCloudDarkColor, uCloudLightColor, light);\n float a = min(d * uDensity * growth, 1.0);\n accum += (1.0 - alpha) * sampleColor * a;\n alpha += (1.0 - alpha) * a;\n }\n t += uStepSize * growth;\n if (alpha > 0.95) break;\n }\n vec3 color = mix(skyColor, accum, alpha);\n color *= uExposure;\n color = pow(color, vec3(0.9));\n oColor = vec4(color, 1.0);\n}\n";
9
+ export declare const NEBULA_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform vec2 uResolution; // framebuffer size in pixels; both components > 0\nuniform vec3 uColor; // overall tint / color grade; [1,1,1] = untinted\nuniform float uBrightness; // final glow multiplier; 1.0 = stock\nuniform float uSpeed; // drift + rotation rate; 1.0 = stock, 0 freezes\nuniform float uTwinkleSpeed; // star color-cycle rate; 1.0 = stock\nuniform float uScale; // starfield zoom / density; >1 = more, smaller stars\nuniform float uStarGlow; // star-core size; 1.0 = stock\n\nin highp vec2 vUv;\nout vec4 oColor;\n\nconst float PI = 3.14159265;\nconst float MIN_DIVIDE = 64.0;\nconst float MAX_DIVIDE = 0.01;\n// Number of stacked starfield layers. Compile-time constant so the layer\n// loop has a fixed integer bound (cross-compile-safe; no float loop counter).\n// 8 (was 12) for low-end-mobile cost (issue #39); the work is linear in the\n// count and dimByDensity rebalances per-star brightness automatically.\nconst int STARFIELD_LAYERS_COUNT = 8;\n\nmat2 Rotate(float angle) {\n float s = sin(angle);\n float c = cos(angle);\n return mat2(c, -s, s, c);\n}\n\nfloat Star(vec2 uv, float flaresize, float rotAngle, float randomN) {\n float d = length(uv);\n // The concentric fade at the bottom is exactly 0 for d >= 1.0; the star is\n // invisible there, so skip everything (issue #39: a large share of the 3x3\n // neighbor sweep lands outside this radius; the cull is output-identical).\n if (d >= 1.0) return 0.0;\n // Star core. Guard the division: length(uv) can be exactly 0 at a cell\n // center, which yields inf/NaN under Metal. max(d, 1e-4) caps the core\n // brightness without visibly changing the look (the concentric\n // smoothstep fade below already clamps it).\n float starcore = 0.05 * uStarGlow / max(d, 1e-4);\n // Flares exist only on the brightest stars: flaresize is exactly 0 below the\n // smoothstep(0.9, 1.0, size) knee (~90% of cells), and both Rotates feed\n // nothing but the flares. Skipping the block is output-identical, and\n // flaresize is constant per cell, so the branch is coherent (issue #39).\n if (flaresize > 0.0) {\n uv *= Rotate(-2.0 * PI * rotAngle);\n float flareMax = 1.0;\n\n // flares\n float starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * flaresize;\n uv *= Rotate(PI * 0.25);\n starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * 0.3 * flaresize;\n }\n // light can't go forever, fade it concentrically.\n starcore *= smoothstep(1.0, 0.05, d);\n return starcore;\n}\n\nfloat PseudoRandomizer(vec2 p) {\n // not really random, but it looks random.\n p = fract(p * vec2(123.45, 345.67));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nvec3 StarFieldLayer(vec2 uv, float rotAngle) {\n vec3 col = vec3(0.0);\n\n vec2 gv = fract(uv) - 0.5;\n vec2 id = floor(uv);\n\n float deltaTimeTwinkle = uTime * 0.35 * uTwinkleSpeed;\n\n // sweep the 8 neighbors plus the home cell so stars are not clipped at\n // cell borders. Constant 3x3 bounds.\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n vec2 offset = vec2(float(x), float(y));\n\n float randomN = PseudoRandomizer(id + offset); // 0..1\n float randoX = randomN - 0.5;\n float randoY = fract(randomN * 45.0) - 0.5;\n vec2 randomPosition = gv - offset - vec2(randoX, randoY);\n // fract trick: random sizes\n float size = fract(randomN * 1356.33);\n float flareSwitch = smoothstep(0.9, 1.0, size);\n float star = Star(randomPosition, flareSwitch, rotAngle, randomN);\n\n // fract trick: random colors\n float randomStarColorSeed = fract(randomN * 2150.0) * (3.0 * PI) * deltaTimeTwinkle;\n vec3 color = sin(vec3(0.7, 0.3, 0.9) * randomStarColorSeed);\n\n // compress\n color = color * (0.4 * sin(deltaTimeTwinkle)) + 0.6;\n // filter\n color = color * vec3(1.0, 0.1, 0.9 + size);\n float dimByDensity = 15.0 / float(STARFIELD_LAYERS_COUNT);\n col += star * size * color * dimByDensity;\n }\n }\n\n return col;\n}\n\nvoid main() {\n // ShaderToy fragCoord, reconstructed from vUv (see header).\n vec2 fragCoord = vUv * uResolution;\n\n // Normalized pixel coordinates centered at screen middle.\n vec2 uv = (fragCoord - 0.5 * uResolution.xy) / uResolution.y;\n\n float deltaTime = uTime * 0.01 * uSpeed;\n\n vec3 col = vec3(0.0);\n\n float rotAngle = deltaTime * 0.09;\n\n // Layer accumulation. Integer-counted loop replacing the original\n // `for (float i = 0.0; i < 1.0; i += 1.0/COUNT)`. With n in [0, COUNT),\n // i = n/COUNT reproduces the exact same {0, 1/N, 2/N, ...} sequence and\n // the same iteration count, so visual output is unchanged; only the loop\n // form is cross-compile-safe.\n for (int n = 0; n < STARFIELD_LAYERS_COUNT; n++) {\n float i = float(n) / float(STARFIELD_LAYERS_COUNT);\n float layerDepth = fract(i + deltaTime);\n float layerScale = mix(MIN_DIVIDE, MAX_DIVIDE, layerDepth);\n float layerFader = layerDepth * smoothstep(0.1, 1.1, layerDepth);\n float layerOffset = i * (3430.0 + fract(i));\n mat2 layerRot = Rotate(rotAngle * i * -10.0);\n uv *= layerRot;\n vec2 starfieldUv = uv * layerScale * uScale + layerOffset;\n col += StarFieldLayer(starfieldUv, rotAngle) * layerFader;\n }\n\n // Glow + color grade, then opaque procedural background.\n col *= uBrightness * uColor;\n oColor = vec4(col, 1.0);\n}\n";
10
10
  export declare const GODRAYS_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing\nuniform vec2 uResolution; // framebuffer size in pixels\nuniform vec3 uLightColor; // ray tint (linear-ish RGB, 0..1)\nuniform float uRayCount; // number of ray bands\nuniform float uRaySpeed; // drift speed\nuniform float uRayIntensity; // overall brightness / additive strength\nuniform float uRaySoftness; // edge falloff exponent (higher = crisper shafts)\nuniform float uTopGlow; // extra glow concentrated near the top\nuniform float uFadeDistance; // vertical falloff from the top\nuniform float uWobbleAmount; // horizontal wobble magnitude\nuniform float uWobbleSpeed; // wobble animation speed\n\nin highp vec2 vUv;\nout vec4 oColor;\n\nfloat hash(float n) {\n return fract(sin(n) * 43758.5453123);\n}\n\nfloat noise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n\n f = f * f * (3.0 - 2.0 * f);\n\n float a = hash(i.x + i.y * 57.0);\n float b = hash(i.x + 1.0 + i.y * 57.0);\n float c = hash(i.x + (i.y + 1.0) * 57.0);\n float d = hash(i.x + 1.0 + (i.y + 1.0) * 57.0);\n\n return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nvoid main() {\n // vUv is already 0..1 with bottom-left origin; this is the Shadertoy `uv`.\n vec2 uv = vUv;\n\n float aspect = uResolution.x / uResolution.y;\n vec2 p = uv;\n p.x = (p.x - 0.5) * aspect + 0.5;\n\n float t = uTime;\n float fromTop = 1.0 - uv.y;\n\n float verticalFade = exp(-fromTop * uFadeDistance);\n float topGlow = exp(-fromTop * 8.0) * uTopGlow;\n\n float wobble =\n (noise(vec2(uv.y * 3.0, t * uWobbleSpeed)) - 0.5) * uWobbleAmount;\n\n float rayCoord = (p.x + wobble) * uRayCount;\n\n float raysA = sin(rayCoord + t * uRaySpeed);\n float raysB = sin(rayCoord * 1.73 - t * uRaySpeed * 0.7);\n\n float rays = raysA * 0.65 + raysB * 0.35;\n rays = rays * 0.5 + 0.5;\n rays = pow(rays, uRaySoftness);\n\n float shimmer = noise(vec2(uv.x * 10.0, uv.y * 4.0 - t * 0.3));\n rays *= mix(0.75, 1.25, shimmer);\n\n float alpha = rays * verticalFade * uRayIntensity;\n alpha += topGlow * uRayIntensity;\n alpha = clamp(alpha, 0.0, 1.0);\n\n // Premultiplied additive output: rgb already scaled by alpha.\n vec3 rayColor = uLightColor * alpha;\n oColor = vec4(rayColor, alpha);\n}\n";
11
11
  export declare const FIREFLIES_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime;\nuniform vec2 uResolution;\nuniform float uGlowSize;\nuniform float uDotSize;\nuniform float uSpeed;\nuniform float uTwinkle;\nuniform vec3 uColor;\n\nin highp vec2 vUv;\nout vec4 oColor;\n\n#define FIREFLY_COUNT 36\n\nfloat hash(float n) {\n return fract(sin(n) * 43758.5453123);\n}\n\nvec2 fireflyPos(float id, float t) {\n vec2 base = vec2(hash(id * 12.7), hash(id * 31.3));\n float a = hash(id * 5.1) * 6.28318;\n float b = hash(id * 9.7) * 6.28318;\n vec2 drift = vec2(sin(t * uSpeed + a), cos(t * uSpeed * 0.73 + b)) * 0.12;\n return fract(base + drift);\n}\n\nvoid main() {\n vec2 uv = vUv;\n vec2 p = uv;\n p.x *= uResolution.x / uResolution.y;\n vec3 color = vec3(0.0);\n float alpha = 0.0;\n for (int i = 0; i < FIREFLY_COUNT; i++) {\n float id = float(i);\n vec2 pos = fireflyPos(id, uTime);\n pos.x *= uResolution.x / uResolution.y;\n float d = length(p - pos);\n float phase = hash(id * 17.1) * 6.28318;\n float pulse = 0.5 + 0.5 * sin(uTime * uTwinkle + phase);\n pulse = pulse * pulse * sqrt(pulse); // pow(pulse, 2.5): a non-integer pow is ~2 transcendentals on mobile; this is 1 sqrt + 2 muls\n float glow = exp(-d * d / (uGlowSize * uGlowSize));\n float core = smoothstep(uDotSize, 0.0, d);\n float intensity = pulse * (glow * 0.55 + core * 1.4);\n color += uColor * intensity;\n alpha += intensity * 0.55;\n }\n alpha = clamp(alpha, 0.0, 1.0);\n color = clamp(color, 0.0, 1.0);\n oColor = vec4(color, alpha);\n}\n";
12
- export declare const SIMIANLIGHTS_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform vec2 uResolution; // framebuffer size in pixels; both components > 0\nuniform vec3 uColor; // overall tint / color grade; [1,1,1] = untinted\nuniform float uBrightness; // final glow multiplier; 1.0 = stock\nuniform float uSpeed; // drift + rotation rate; 1.0 = stock, 0 freezes\nuniform float uTwinkleSpeed; // star color-cycle rate; 1.0 = stock\nuniform float uScale; // starfield zoom / density; >1 = more, smaller stars\nuniform float uStarGlow; // star-core size; 1.0 = stock\n\nin highp vec2 vUv;\nout vec4 oColor;\n\nconst float PI = 3.14159265;\nconst float MIN_DIVIDE = 3.0;\nconst float MAX_DIVIDE = 0.01;\n// Number of stacked starfield layers. Compile-time constant so the layer\n// loop has a fixed integer bound (cross-compile-safe; no float loop counter).\nconst int STARFIELD_LAYERS_COUNT = 4;\n\nmat2 Rotate(float angle) {\n float s = sin(angle);\n float c = cos(angle);\n return mat2(c, -s, s, c);\n}\n\nfloat Star(vec2 uv, float flaresize, float rotAngle, float randomN) {\n float d = length(uv);\n // Star core. Guard the division: length(uv) can be exactly 0 at a cell\n // center, which yields inf/NaN under Metal. max(d, 1e-4) caps the core\n // brightness without visibly changing the look (the concentric\n // smoothstep fade below already clamps it).\n float starcore = 0.09 * uStarGlow / max(d, 1e-4);\n uv *= Rotate(-2.0 * PI * rotAngle);\n float flareMax = 1.0;\n\n // flares\n float starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * flaresize;\n uv *= Rotate(PI * 0.25);\n starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * 0.3 * flaresize;\n // light can't go forever, fade it concentrically.\n starcore *= smoothstep(1.0, 0.05, d);\n return starcore;\n}\n\nfloat PseudoRandomizer(vec2 p) {\n // not really random, but it looks random.\n p = fract(p * vec2(123.45, 345.67));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nvec3 StarFieldLayer(vec2 uv, float rotAngle) {\n vec3 col = vec3(0.0);\n\n vec2 gv = fract(uv) - 0.5;\n vec2 id = floor(uv);\n\n float deltaTimeTwinkle = uTime * 0.35 * uTwinkleSpeed;\n\n // sweep the 8 neighbors plus the home cell so stars are not clipped at\n // cell borders. Constant 3x3 bounds.\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n vec2 offset = vec2(float(x), float(y));\n\n float randomN = PseudoRandomizer(id + offset); // 0..1\n float randoX = randomN - 0.5;\n float randoY = fract(randomN * 45.0) - 0.5;\n vec2 randomPosition = gv - offset - vec2(randoX, randoY);\n // fract trick: random sizes\n float size = fract(randomN * 1356.33);\n float flareSwitch = smoothstep(0.9, 1.0, size);\n float star = Star(randomPosition, flareSwitch, rotAngle, randomN);\n\n // fract trick: random colors\n float randomStarColorSeed = fract(randomN * 2150.0) * (3.0 * PI) * deltaTimeTwinkle;\n vec3 color = sin(vec3(0.7, 0.3, 0.9) * randomStarColorSeed);\n\n // compress\n color = color * (0.4 * sin(deltaTimeTwinkle)) + 0.6;\n // filter\n color = color * vec3(1.0, 0.1, 0.9 + size);\n float dimByDensity = 15.0 / float(STARFIELD_LAYERS_COUNT);\n col += star * size * color * dimByDensity;\n }\n }\n\n return col;\n}\n\nvoid main() {\n // ShaderToy fragCoord, reconstructed from vUv (see header).\n vec2 fragCoord = vUv * uResolution;\n\n // Normalized pixel coordinates centered at screen middle.\n vec2 uv = (fragCoord - 0.5 * uResolution.xy) / uResolution.y;\n\n float deltaTime = uTime * 0.01 * uSpeed;\n\n vec3 col = vec3(0.0);\n\n float rotAngle = deltaTime * 0.09;\n\n // Layer accumulation. Integer-counted loop replacing the original\n // `for (float i = 0.0; i < 1.0; i += 1.0/COUNT)`. With n in [0, COUNT),\n // i = n/COUNT reproduces the exact same {0, 1/N, 2/N, ...} sequence and\n // the same iteration count, so visual output is unchanged; only the loop\n // form is cross-compile-safe.\n for (int n = 0; n < STARFIELD_LAYERS_COUNT; n++) {\n float i = float(n) / float(STARFIELD_LAYERS_COUNT);\n float layerDepth = fract(i + deltaTime);\n float layerScale = mix(MIN_DIVIDE, MAX_DIVIDE, layerDepth);\n float layerFader = layerDepth * smoothstep(0.1, 1.1, layerDepth);\n float layerOffset = i * (3430.0 + fract(i));\n mat2 layerRot = Rotate(rotAngle * i * -10.0);\n uv *= layerRot;\n vec2 starfieldUv = uv * layerScale * uScale + layerOffset;\n col += StarFieldLayer(starfieldUv, rotAngle) * layerFader;\n }\n\n // Glow + color grade, then opaque procedural background.\n col *= uBrightness * uColor;\n oColor = vec4(col, 1.0);\n}\n";
12
+ export declare const SIMIANLIGHTS_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform vec2 uResolution; // framebuffer size in pixels; both components > 0\nuniform vec3 uColor; // overall tint / color grade; [1,1,1] = untinted\nuniform float uBrightness; // final glow multiplier; 1.0 = stock\nuniform float uSpeed; // drift + rotation rate; 1.0 = stock, 0 freezes\nuniform float uTwinkleSpeed; // star color-cycle rate; 1.0 = stock\nuniform float uScale; // starfield zoom / density; >1 = more, smaller stars\nuniform float uStarGlow; // star-core size; 1.0 = stock\n\nin highp vec2 vUv;\nout vec4 oColor;\n\nconst float PI = 3.14159265;\nconst float MIN_DIVIDE = 3.0;\nconst float MAX_DIVIDE = 0.01;\n// Number of stacked starfield layers. Compile-time constant so the layer\n// loop has a fixed integer bound (cross-compile-safe; no float loop counter).\nconst int STARFIELD_LAYERS_COUNT = 4;\n\nmat2 Rotate(float angle) {\n float s = sin(angle);\n float c = cos(angle);\n return mat2(c, -s, s, c);\n}\n\nfloat Star(vec2 uv, float flaresize, float rotAngle, float randomN) {\n float d = length(uv);\n // The concentric fade at the bottom is exactly 0 for d >= 1.0; the star is\n // invisible there, so skip everything (issue #39: a large share of the 3x3\n // neighbor sweep lands outside this radius; the cull is output-identical).\n if (d >= 1.0) return 0.0;\n // Star core. Guard the division: length(uv) can be exactly 0 at a cell\n // center, which yields inf/NaN under Metal. max(d, 1e-4) caps the core\n // brightness without visibly changing the look (the concentric\n // smoothstep fade below already clamps it).\n float starcore = 0.09 * uStarGlow / max(d, 1e-4);\n // Flares exist only on the brightest stars: flaresize is exactly 0 below the\n // smoothstep(0.9, 1.0, size) knee (~90% of cells), and both Rotates feed\n // nothing but the flares. Skipping the block is output-identical, and\n // flaresize is constant per cell, so the branch is coherent (issue #39).\n if (flaresize > 0.0) {\n uv *= Rotate(-2.0 * PI * rotAngle);\n float flareMax = 1.0;\n\n // flares\n float starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * flaresize;\n uv *= Rotate(PI * 0.25);\n starflares = max(0.0, flareMax - abs(uv.x * uv.y * 3000.0));\n starcore += starflares * 0.3 * flaresize;\n }\n // light can't go forever, fade it concentrically.\n starcore *= smoothstep(1.0, 0.05, d);\n return starcore;\n}\n\nfloat PseudoRandomizer(vec2 p) {\n // not really random, but it looks random.\n p = fract(p * vec2(123.45, 345.67));\n p += dot(p, p + 45.32);\n return fract(p.x * p.y);\n}\n\nvec3 StarFieldLayer(vec2 uv, float rotAngle) {\n vec3 col = vec3(0.0);\n\n vec2 gv = fract(uv) - 0.5;\n vec2 id = floor(uv);\n\n float deltaTimeTwinkle = uTime * 0.35 * uTwinkleSpeed;\n\n // sweep the 8 neighbors plus the home cell so stars are not clipped at\n // cell borders. Constant 3x3 bounds.\n for (int y = -1; y <= 1; y++) {\n for (int x = -1; x <= 1; x++) {\n vec2 offset = vec2(float(x), float(y));\n\n float randomN = PseudoRandomizer(id + offset); // 0..1\n float randoX = randomN - 0.5;\n float randoY = fract(randomN * 45.0) - 0.5;\n vec2 randomPosition = gv - offset - vec2(randoX, randoY);\n // fract trick: random sizes\n float size = fract(randomN * 1356.33);\n float flareSwitch = smoothstep(0.9, 1.0, size);\n float star = Star(randomPosition, flareSwitch, rotAngle, randomN);\n\n // fract trick: random colors\n float randomStarColorSeed = fract(randomN * 2150.0) * (3.0 * PI) * deltaTimeTwinkle;\n vec3 color = sin(vec3(0.7, 0.3, 0.9) * randomStarColorSeed);\n\n // compress\n color = color * (0.4 * sin(deltaTimeTwinkle)) + 0.6;\n // filter\n color = color * vec3(1.0, 0.1, 0.9 + size);\n float dimByDensity = 15.0 / float(STARFIELD_LAYERS_COUNT);\n col += star * size * color * dimByDensity;\n }\n }\n\n return col;\n}\n\nvoid main() {\n // ShaderToy fragCoord, reconstructed from vUv (see header).\n vec2 fragCoord = vUv * uResolution;\n\n // Normalized pixel coordinates centered at screen middle.\n vec2 uv = (fragCoord - 0.5 * uResolution.xy) / uResolution.y;\n\n float deltaTime = uTime * 0.01 * uSpeed;\n\n vec3 col = vec3(0.0);\n\n float rotAngle = deltaTime * 0.09;\n\n // Layer accumulation. Integer-counted loop replacing the original\n // `for (float i = 0.0; i < 1.0; i += 1.0/COUNT)`. With n in [0, COUNT),\n // i = n/COUNT reproduces the exact same {0, 1/N, 2/N, ...} sequence and\n // the same iteration count, so visual output is unchanged; only the loop\n // form is cross-compile-safe.\n for (int n = 0; n < STARFIELD_LAYERS_COUNT; n++) {\n float i = float(n) / float(STARFIELD_LAYERS_COUNT);\n float layerDepth = fract(i + deltaTime);\n float layerScale = mix(MIN_DIVIDE, MAX_DIVIDE, layerDepth);\n float layerFader = layerDepth * smoothstep(0.1, 1.1, layerDepth);\n float layerOffset = i * (3430.0 + fract(i));\n mat2 layerRot = Rotate(rotAngle * i * -10.0);\n uv *= layerRot;\n vec2 starfieldUv = uv * layerScale * uScale + layerOffset;\n col += StarFieldLayer(starfieldUv, rotAngle) * layerFader;\n }\n\n // Glow + color grade, then opaque procedural background.\n col *= uBrightness * uColor;\n oColor = vec4(col, 1.0);\n}\n";
13
13
  export declare const ANAMORPHIC_LENSFLARE_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform float uFlareX; // flare X position, 0..1 (drifts slowly around this)\nuniform float uFlareY; // flare Y position, 0..1 (0 = bottom)\nuniform float uIntensity; // overall brightness multiplier\nuniform float uStreakLength; // horizontal streak reach; higher = longer\nuniform float uStreakWidth; // main streak vertical tightness; higher = thinner\nuniform float uGhostStrength; // optical-ghost strength along the flare axis\nuniform vec3 uWarmColor; // core / warm streak tint\nuniform vec3 uBlueColor; // halo / wide-streak tint\nuniform vec3 uPinkColor; // secondary streak / ghost tint\n\nin highp vec2 vUv;\nout vec4 oColor;\n\nfloat softOrb(vec2 uv, vec2 center, float radius) {\n float d = length(uv - center);\n float q = d / radius; // pow(q, 2.0) -> q*q; spirv-opt does not strength-reduce it\n return exp(-q * q);\n}\n\nfloat softStreak(vec2 uv, vec2 center, float width, float length) {\n float yFalloff = exp(-abs(uv.y - center.y) * width);\n float xFalloff = exp(-abs(uv.x - center.x) * length);\n return yFalloff * xFalloff;\n}\n\nvoid main() {\n // ShaderToy uv = fragCoord / iResolution; identical to vUv here.\n vec2 uv = vUv;\n\n // Slow horizontal drift.\n float horizontalDrift = sin(uTime * 0.18) * 0.10;\n vec2 flarePos = vec2(uFlareX + horizontalDrift, uFlareY);\n\n vec3 col = vec3(0.0);\n float alpha = 0.0;\n\n // Main source.\n float core = softOrb(uv, flarePos, 0.022);\n float bloom = softOrb(uv, flarePos, 0.095);\n float outerHalo = softOrb(uv, flarePos, 0.28);\n\n col += vec3(1.0) * core * 1.8;\n col += uWarmColor * bloom * 0.95;\n col += uBlueColor * outerHalo * 0.16;\n\n alpha += core * 0.45;\n alpha += bloom * 0.22;\n alpha += outerHalo * 0.045;\n\n // Moving streak intensity.\n float sweepGlow = 0.85 + 0.15 * sin(uTime * 0.7 + uv.x * 8.0);\n\n // Main + wide anamorphic streaks.\n float mainStreak = softStreak(uv, flarePos, uStreakWidth, uStreakLength);\n float wideStreak = softStreak(uv, flarePos, 70.0, uStreakLength * 0.65);\n\n col += uWarmColor * mainStreak * 1.15 * sweepGlow;\n col += uBlueColor * wideStreak * 0.26 * sweepGlow;\n\n alpha += mainStreak * 0.18;\n alpha += wideStreak * 0.055;\n\n // Secondary colored streaks.\n float upperLine = softStreak(uv, flarePos + vec2(0.0, 0.012), 260.0, uStreakLength * 0.7);\n float lowerLine = softStreak(uv, flarePos - vec2(0.0, 0.010), 240.0, uStreakLength * 0.8);\n\n col += uBlueColor * upperLine * 0.22;\n col += uPinkColor * lowerLine * 0.16;\n\n alpha += (upperLine + lowerLine) * 0.035;\n\n // Optical ghosts along the line from the flare through screen center.\n vec2 center = vec2(0.5);\n vec2 axis = center - flarePos;\n\n vec2 ghost1 = flarePos + axis * 0.45;\n vec2 ghost2 = flarePos + axis * 0.85;\n vec2 ghost3 = flarePos + axis * 1.28;\n vec2 ghost4 = flarePos - axis * 0.35;\n\n float g1 = softOrb(uv, ghost1, 0.070);\n float g2 = softOrb(uv, ghost2, 0.115);\n float g3 = softOrb(uv, ghost3, 0.055);\n float g4 = softOrb(uv, ghost4, 0.095);\n\n col += uPinkColor * g1 * 0.18 * uGhostStrength;\n col += uBlueColor * g2 * 0.15 * uGhostStrength;\n col += uWarmColor * g3 * 0.22 * uGhostStrength;\n col += uBlueColor * g4 * 0.10 * uGhostStrength;\n\n alpha += g1 * 0.040 * uGhostStrength;\n alpha += g2 * 0.035 * uGhostStrength;\n alpha += g3 * 0.050 * uGhostStrength;\n alpha += g4 * 0.030 * uGhostStrength;\n\n // Tiny shimmer to keep it alive.\n float shimmer = 0.97 + 0.03 * sin(uTime * 2.1);\n\n col *= uIntensity * shimmer;\n alpha *= uIntensity * shimmer;\n\n alpha = clamp(alpha, 0.0, 1.0);\n oColor = vec4(col, alpha);\n}\n";
14
14
  export declare const LIGHT_BEAMS_AND_MOTES_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform float uSpeed; // animation rate; 1.0 = stock, 0 freezes the field\nuniform float uBeamSoftness; // beam polygon edge softness\nuniform float uOverlayAlpha; // overall overlay opacity, applied to final alpha\n\n// Per-beam: a row-major quad (TL, TR, BL, BR; y-up), a color, a fill strength\n// (absolute), and an on/off flag. A disabled beam's quadMask + sins do not execute\n// at all (the flag is uniform, so the branch is coherent across every fragment) --\n// that is how you stop paying for beams you are not using.\nuniform vec2 uBeam1Poly[4];\nuniform vec3 uBeam1Color;\nuniform float uBeam1Alpha;\nuniform float uBeam1On;\nuniform vec2 uBeam2Poly[4];\nuniform vec3 uBeam2Color;\nuniform float uBeam2Alpha;\nuniform float uBeam2On;\nuniform vec2 uBeam3Poly[4];\nuniform vec3 uBeam3Color;\nuniform float uBeam3Alpha;\nuniform float uBeam3On;\n\nuniform float uMoteAlpha; // mote brightness (absolute)\nuniform float uGlowSize; // mote glow radius, in mote-size multiples\nuniform float uMoteCount; // active motes (<= MOTE_COUNT); a coherent break trims the loop\n\nin highp vec2 vUv;\nout vec4 oColor;\n\n// ---------- Mote controls (internal constants) ----------\n#define MOTE_COUNT 128 // loop bound: compile-time constant, not a uniform\n#define DRIFT_SPEED 0.060\n#define FALL_SPEED 0.012\n#define SWIRL_AMOUNT 0.065\n#define TURBULENCE 0.030\n#define MOTE_SIZE_MIN 0.0013\n#define MOTE_SIZE_MAX 0.0048\n\nfloat hash1(float n) {\n return fract(sin(n) * 43758.5453123);\n}\n\nvec2 hash2(float n) {\n return vec2(hash1(n + 11.17), hash1(n + 47.83));\n}\n\nfloat softMote(vec2 uv, vec2 center, float radius) {\n float d = length(uv - center);\n float q = d / radius; // pow(q, 2.0) -> q*q; spirv-opt does not strength-reduce it\n return exp(-q * q);\n}\n\n// Soft convex quad mask, winding-AGNOSTIC: the corners are user-draggable, so the\n// perimeter can wind either way. A point is inside when it is on the same side of\n// all four edges, whichever side that is; product the positive-side smoothsteps and\n// the negative-side ones and keep the larger.\nfloat quadMask(vec2 p, vec2 a, vec2 b, vec2 c, vec2 d, float softness) {\n vec2 e0 = b - a;\n vec2 e1 = c - b;\n vec2 e2 = d - c;\n vec2 e3 = a - d;\n float s0 = e0.x * (p.y - a.y) - e0.y * (p.x - a.x);\n float s1 = e1.x * (p.y - b.y) - e1.y * (p.x - b.x);\n float s2 = e2.x * (p.y - c.y) - e2.y * (p.x - c.x);\n float s3 = e3.x * (p.y - d.y) - e3.y * (p.x - d.x);\n float inNeg =\n smoothstep(-softness, softness, -s0) *\n smoothstep(-softness, softness, -s1) *\n smoothstep(-softness, softness, -s2) *\n smoothstep(-softness, softness, -s3);\n float inPos =\n smoothstep(-softness, softness, s0) *\n smoothstep(-softness, softness, s1) *\n smoothstep(-softness, softness, s2) *\n smoothstep(-softness, softness, s3);\n return max(inNeg, inPos);\n}\n\n// Subtle animated variation inside each beam.\nfloat beamTexture(vec2 uv, float seed) {\n float t = uTime * uSpeed;\n float broadBands = 0.55 + 0.45 * sin(uv.x * 7.0 + uv.y * 4.0 + t * 0.06 + seed);\n float fineBands = 0.75 + 0.25 * sin(uv.x * 23.0 - uv.y * 11.0 + t * 0.11 + seed * 2.7);\n return mix(0.65, 1.0, broadBands * fineBands);\n}\n\n// Geometric coverage of one row-major beam quad at uv (mask * texture, 0..~1), with\n// the row-major -> perimeter reorder folded in. Independent of the beam's alpha.\nfloat beamShape(vec2 uv, vec2 poly[4], float seed) {\n return quadMask(uv, poly[0], poly[1], poly[3], poly[2], uBeamSoftness) * beamTexture(uv, seed);\n}\n\n// Evaluate the three beams once. Returns the GEOMETRIC coverage sum (used to gate +\n// brighten motes and to drive the haze, so per-beam alpha never dims the motes);\n// writes `color` (the geometry-weighted beam hue, for fill and motes) and `litSum`\n// (the alpha-weighted amount, the actual fill brightness/opacity).\nfloat evalBeams(vec2 uv, out vec3 color, out float litSum) {\n float a1 = 0.0;\n float a2 = 0.0;\n float a3 = 0.0;\n if (uBeam1On > 0.5) a1 = beamShape(uv, uBeam1Poly, 1.0);\n if (uBeam2On > 0.5) a2 = beamShape(uv, uBeam2Poly, 8.0);\n if (uBeam3On > 0.5) a3 = beamShape(uv, uBeam3Poly, 14.0);\n\n float geomSum = a1 + a2 + a3;\n color = (uBeam1Color * a1 + uBeam2Color * a2 + uBeam3Color * a3) / max(geomSum, 0.0001);\n litSum = a1 * uBeam1Alpha + a2 * uBeam2Alpha + a3 * uBeam3Alpha;\n return geomSum;\n}\n\n// Accumulate the dust motes for ALL on beams in ONE loop. Each mote is round-\n// robined to an on beam (mote n -> the (n mod nActive)-th on beam) and spawned in\n// THAT beam's (u,v) space: every mote lands in a beam, takes the beam's color, and\n// gets a cheap (u,v) edge falloff -- no screen-space scatter to cull, no per-mote\n// quadMask. n and nActive are uniform across fragments, so the beam pick is\n// COHERENT (same for every pixel), not a divergent per-pixel branch. Motes drift\n// ALONG the beam (fall in v, swirl in u) and wrap, fading at the boundaries so the\n// wrap is never a visible pop. Total mote count is uMoteCount regardless of how\n// many beams are on -- the on beams share the budget.\nvoid addMotes(vec2 uv, inout vec3 col, inout float alpha) {\n float nActive = uBeam1On + uBeam2On + uBeam3On; // on-flags are 0/1\n if (nActive < 0.5) return; // no beams -> no motes\n\n // Motes may spill past the polygon edge by ~softness so the soft fringe is\n // populated; the (u,v) falloff dims them there.\n float fuzz = clamp(uBeamSoftness * 2.0, 0.03, 0.35);\n float slot = 0.0; // round-robin cursor over the on beams (a wrapped counter, no per-mote mod)\n\n for (int n = 0; n < MOTE_COUNT; n++) {\n if (float(n) >= uMoteCount) break; // runtime-tunable mote count (coherent break)\n float seed = float(n) * 91.73;\n\n // Round-robin: this mote goes to the slot-th on beam. Walk the beams,\n // counting on ones; the slot-th match wins.\n float picked = 0.0;\n vec2 tl = vec2(0.0);\n vec2 tr = vec2(0.0);\n vec2 bl = vec2(0.0);\n vec2 br = vec2(0.0);\n vec3 color = vec3(0.0);\n if (uBeam1On > 0.5) {\n if (abs(picked - slot) < 0.5) {\n tl = uBeam1Poly[0]; tr = uBeam1Poly[1]; bl = uBeam1Poly[2]; br = uBeam1Poly[3];\n color = uBeam1Color;\n }\n picked += 1.0;\n }\n if (uBeam2On > 0.5) {\n if (abs(picked - slot) < 0.5) {\n tl = uBeam2Poly[0]; tr = uBeam2Poly[1]; bl = uBeam2Poly[2]; br = uBeam2Poly[3];\n color = uBeam2Color;\n }\n picked += 1.0;\n }\n if (uBeam3On > 0.5) {\n if (abs(picked - slot) < 0.5) {\n tl = uBeam3Poly[0]; tr = uBeam3Poly[1]; bl = uBeam3Poly[2]; br = uBeam3Poly[3];\n color = uBeam3Color;\n }\n picked += 1.0;\n }\n\n float depth = hash1(seed + 3.0);\n float size = mix(MOTE_SIZE_MIN, MOTE_SIZE_MAX, depth);\n float speed = mix(0.45, 1.35, hash1(seed + 5.0));\n float t = uTime * uSpeed * speed;\n\n // (u,v) in beam space; drift = swirl in u, fall in v. fract wraps within\n // the beam, so a mote that falls out the spread end reappears at the source.\n vec2 g = hash2(seed);\n g.x += sin(t * DRIFT_SPEED * 1.7 + seed) * SWIRL_AMOUNT;\n g.x += sin(t * DRIFT_SPEED * 3.9 + seed * 0.41) * TURBULENCE;\n g.y += uTime * uSpeed * FALL_SPEED * speed * 6.0;\n g.y += sin(t * DRIFT_SPEED * 2.4 + seed * 0.37) * SWIRL_AMOUNT * 0.55;\n g = fract(g);\n\n // Expand to [-fuzz, 1+fuzz] so motes populate the soft fringe, then map\n // bilinearly onto the quad.\n vec2 q = g * (1.0 + 2.0 * fuzz) - fuzz;\n vec2 pos = mix(mix(tl, tr, q.x), mix(bl, br, q.x), q.y);\n\n // Cheap soft-edge falloff in (u,v), replacing the per-mote quadMask.\n float edge =\n smoothstep(0.0, fuzz, q.x) * smoothstep(0.0, fuzz, 1.0 - q.x) *\n smoothstep(0.0, fuzz, q.y) * smoothstep(0.0, fuzz, 1.0 - q.y);\n\n float mote = softMote(uv, pos, size);\n float core = softMote(uv, pos, size * 0.42);\n float glow = softMote(uv, pos, size * uGlowSize);\n\n float shimmer =\n 0.72 + 0.28 * sin(uTime * uSpeed * mix(0.22, 0.95, hash1(seed + 9.0)) + seed);\n float strength = mix(0.15, 1.0, depth) * shimmer * edge * uMoteAlpha;\n\n col += color * glow * strength * 0.12;\n col += color * mote * strength * 0.36;\n col += vec3(1.0) * core * strength * 0.10;\n\n alpha += glow * strength * 0.030;\n alpha += mote * strength * 0.105;\n alpha += core * strength * 0.110;\n\n slot += 1.0;\n if (slot >= nActive) slot = 0.0; // wrap the round-robin cursor\n }\n}\n\nvoid main() {\n // vUv is already 0..1 with bottom-left origin; this is the Shadertoy uv.\n vec2 uv = vUv;\n\n vec3 col = vec3(0.0);\n float alpha = 0.0;\n\n vec3 beamColor;\n float litSum;\n float cover = evalBeams(uv, beamColor, litSum);\n\n col += beamColor * litSum; // litSum carries each beam's per-beam alpha\n alpha += litSum * 0.45;\n\n // One loop; each mote is round-robined into an on beam and spawned there.\n addMotes(uv, col, alpha);\n\n float haze =\n cover * cover * (0.6 + 0.4 * sin(uv.x * 8.0 + uv.y * 5.0 + uTime * uSpeed * 0.08));\n col += beamColor * haze * 0.018;\n alpha += haze * 0.010;\n\n alpha = clamp(alpha * uOverlayAlpha, 0.0, 1.0);\n oColor = vec4(col, alpha);\n}\n";
15
15
  export declare const CORPORATE_BLOBS_FRAG_SRC = "#version 300 es\nprecision highp float;\n\nuniform float uTime; // seconds, monotonically increasing; range [0, inf)\nuniform vec2 uResolution; // framebuffer size in pixels; both components > 0\nuniform vec3 uColor; // overall tint / color grade; [1,1,1] = stock colors\nuniform float uGlobalAlpha; // overall blob opacity; stock 0.58\nuniform float uScale; // global blob size multiplier; stock 2.55\nuniform float uEdgePull; // pushes blobs outward from center; stock 0.32\nuniform float uCenterClear; // radius around center that repels blobs; stock 0.42\nuniform float uMotionAmount; // positional drift magnitude; 1.0 = stock, 0 = still\nuniform float uMotionSpeed; // drift + morph rate; 1.0 = stock, 0 freezes motion\nuniform float uEdgeSoftness; // blob edge falloff; stock 0.024\n// Per-blob base colors, multiplied by uColor at output. Defaults (the stock\n// brand palette) live in CORPORATE_BLOBS_CONTROLS.\nuniform vec3 uBlobColor1; // stock: light blue\nuniform vec3 uBlobColor2; // stock: dark green\nuniform vec3 uBlobColor3; // stock: yellow\nuniform vec3 uBlobColor4; // stock: orange\nuniform vec3 uBlobColor5; // stock: light green\nuniform vec3 uBlobColor6; // stock: magenta\nuniform vec3 uBlobColor7; // stock: brown\nuniform vec3 uBlobColor8; // stock: dark blue\n\nin highp vec2 vUv;\nout vec4 oColor;\n\n// BLOB_COUNT must stay a compile-time constant (GLSL ES loop bound).\n#define BLOB_COUNT 8\n\n// Internal animation constants (not tunable; keep the look coherent).\n#define CENTER_CLEAR_PUSH 0.34\n#define SCALE_PULSE_AMOUNT 0.10\n#define SCALE_PULSE_SPEED 0.42\n#define ROTATION_SWAY_AMOUNT 0.12\n#define ROTATION_SWAY_SPEED 0.11\n#define SHAPE_MORPH_SPEED 1.00\n\nstruct Blob {\n vec2 pos;\n float scale;\n float opacity;\n float speed;\n float drift;\n float rotation;\n float variant;\n vec3 color;\n};\n\nBlob getBlob(float i) {\n if (i < 0.5) return Blob(vec2(-1.18, -0.55), 0.62, 0.48, 0.22, 0.14, 0.10, 0.0, uBlobColor1);\n if (i < 1.5) return Blob(vec2( 1.12, -0.35), 0.66, 0.40, 0.18, 0.13, 1.00, 1.0, uBlobColor2);\n if (i < 2.5) return Blob(vec2( 0.95, 0.88), 0.58, 0.44, 0.20, 0.14, 2.20, 2.0, uBlobColor3);\n if (i < 3.5) return Blob(vec2(-0.98, 0.82), 0.56, 0.38, 0.16, 0.12, 0.70, 3.0, uBlobColor4);\n if (i < 4.5) return Blob(vec2( 1.28, 0.28), 0.50, 0.34, 0.24, 0.11, 1.80, 4.0, uBlobColor5);\n if (i < 5.5) return Blob(vec2(-0.25, -1.12), 0.54, 0.36, 0.19, 0.11, 2.60, 5.0, uBlobColor6);\n if (i < 6.5) return Blob(vec2(-1.30, 0.10), 0.48, 0.30, 0.17, 0.12, 0.40, 6.0, uBlobColor7);\n return Blob(vec2( 0.28, 1.18), 0.52, 0.30, 0.14, 0.10, 0.90, 7.0, uBlobColor8);\n}\n\nmat2 rotate2d(float a) {\n float s = sin(a);\n float c = cos(a);\n return mat2(c, -s, s, c);\n}\n\nfloat variantRadius(float angle, float variant, float phase) {\n float r = 1.0;\n\n if (variant < 0.5) {\n r += 0.115 * sin(angle * 2.0 + 0.20 + phase * 0.20);\n r += 0.075 * sin(angle * 3.0 - 1.10 - phase * 0.13);\n r += 0.035 * sin(angle * 5.0 + 2.00 + phase * 0.09);\n } else if (variant < 1.5) {\n r += 0.090 * sin(angle * 2.0 - 0.80 + phase * 0.18);\n r += 0.105 * sin(angle * 3.0 + 0.70 - phase * 0.10);\n r += 0.030 * sin(angle * 6.0 - 1.50 + phase * 0.08);\n } else if (variant < 2.5) {\n r += 0.130 * sin(angle * 2.0 + 1.10 + phase * 0.16);\n r += 0.060 * sin(angle * 4.0 - 0.30 - phase * 0.12);\n r += 0.045 * sin(angle * 5.0 + 2.80 + phase * 0.07);\n } else if (variant < 3.5) {\n r += 0.080 * sin(angle * 2.0 + 2.30 + phase * 0.14);\n r += 0.120 * sin(angle * 3.0 - 0.40 - phase * 0.11);\n r += 0.040 * sin(angle * 7.0 + 1.10 + phase * 0.06);\n } else if (variant < 4.5) {\n r += 0.035 * sin(angle * 2.0 + 0.10 + phase * 0.12);\n r += 0.030 * sin(angle * 3.0 + 1.80 - phase * 0.09);\n r += 0.020 * sin(angle * 5.0 - 0.90 + phase * 0.05);\n } else if (variant < 5.5) {\n r += 0.145 * sin(angle * 2.0 - 1.30 + phase * 0.17);\n r += 0.070 * sin(angle * 3.0 + 2.40 - phase * 0.11);\n r += 0.035 * sin(angle * 5.0 + 0.20 + phase * 0.08);\n } else if (variant < 6.5) {\n r += 0.045 * sin(angle * 2.0 + 1.70 + phase * 0.10);\n r += 0.035 * sin(angle * 4.0 - 2.10 - phase * 0.08);\n r += 0.025 * sin(angle * 6.0 + 0.50 + phase * 0.05);\n } else {\n r += 0.170 * sin(angle * 2.0 + 2.80 + phase * 0.20);\n r += 0.090 * sin(angle * 3.0 - 1.90 - phase * 0.15);\n r += 0.055 * sin(angle * 5.0 + 0.80 + phase * 0.09);\n }\n\n return r;\n}\n\nvec2 applyCenterRepulsor(vec2 center) {\n float d = length(center);\n vec2 dir = normalize(center + vec2(0.0001, 0.0001));\n\n center += dir * uEdgePull;\n\n float centerInfluence = 1.0 - smoothstep(uCenterClear, uCenterClear + 0.35, d);\n center += dir * centerInfluence * CENTER_CLEAR_PUSH;\n\n return center;\n}\n\nfloat animatedScale(float baseScale, float blobIndex, float blobSpeed) {\n float localPhase =\n uTime * SCALE_PULSE_SPEED * (0.65 + blobSpeed * 1.35) +\n blobIndex * 2.731;\n\n float pulseA = sin(localPhase);\n float pulseB = sin(localPhase * 0.47 + blobIndex * 5.13) * 0.45;\n\n float scaleMultiplier = 1.0 + (pulseA + pulseB) * SCALE_PULSE_AMOUNT;\n\n return baseScale * max(0.05, scaleMultiplier);\n}\n\nfloat blobMask(vec2 p, vec2 center, Blob b, float phase, float liveScale) {\n vec2 q = p - center;\n\n vec2 squash = vec2(\n 1.0 + 0.14 * sin(b.variant * 1.91),\n 1.0 + 0.14 * cos(b.variant * 2.37)\n );\n\n float rotationSway =\n sin(uTime * ROTATION_SWAY_SPEED * (0.6 + b.speed) + b.variant * 3.0) *\n ROTATION_SWAY_AMOUNT;\n\n q = rotate2d(b.rotation + rotationSway) * q;\n q /= squash;\n\n float angle = atan(q.y, q.x);\n float dist = length(q);\n\n float r = liveScale * uScale * 0.5 * variantRadius(angle, b.variant, phase);\n\n return 1.0 - smoothstep(r, r + uEdgeSoftness, dist);\n}\n\nvoid main() {\n vec2 uv = vUv;\n\n vec2 p = uv * 2.0 - 1.0;\n p.x *= uResolution.x / uResolution.y;\n\n vec3 blobCol = vec3(0.0);\n float blobAlpha = 0.0;\n\n // Integer-counted loop over a compile-time bound; i is reconstructed as\n // float(n), so the per-blob lookups and phases match the prototype.\n for (int n = 0; n < BLOB_COUNT; n++) {\n float i = float(n);\n Blob b = getBlob(i);\n\n float phase =\n uTime * b.speed * uMotionSpeed * SHAPE_MORPH_SPEED +\n i * 4.137;\n\n vec2 center = b.pos;\n\n center.x += sin(phase * 0.41 + i * 1.70) * b.drift * uMotionAmount;\n center.x += sin(phase * 0.19 + i * 3.10) * b.drift * uMotionAmount * 0.45;\n center.y += cos(phase * 0.33 + i * 2.30) * b.drift * uMotionAmount * 0.75;\n center.y += sin(phase * 0.17 + i * 4.40) * b.drift * uMotionAmount * 0.35;\n\n center = applyCenterRepulsor(center);\n\n float liveScale = animatedScale(b.scale, i, b.speed);\n float mask = blobMask(p, center, b, phase, liveScale);\n\n float inner = pow(mask, 1.35);\n float rim = mask * (1.0 - smoothstep(0.45, 1.0, mask));\n\n vec3 gelColor = b.color * inner + b.color * rim * 0.18;\n float a = mask * b.opacity * uGlobalAlpha;\n\n blobCol += gelColor * a * (1.0 - blobAlpha);\n blobAlpha += a * (1.0 - blobAlpha);\n }\n\n // Premultiplied output; tint grades the (premultiplied) color, not alpha.\n oColor = vec4(blobCol * uColor, blobAlpha);\n}\n";
@@ -1 +1 @@
1
- {"version":3,"file":"shaders.generated.d.ts","sourceRoot":"","sources":["../../web-driver/shaders.generated.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,0NAQhC,CAAC;AAEF,eAAO,MAAM,yBAAyB,oLAQrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,6oBAwBnC,CAAC;AAEF,eAAO,MAAM,wBAAwB,2QAWpC,CAAC;AAEF,eAAO,MAAM,0BAA0B,kgBAiBtC,CAAC;AAEF,eAAO,MAAM,yBAAyB,2eAiBrC,CAAC;AAEF,eAAO,MAAM,eAAe,q6CAkC3B,CAAC;AAEF,eAAO,MAAM,eAAe,klGAuG3B,CAAC;AAEF,eAAO,MAAM,eAAe,iwJAiI3B,CAAC;AAEF,eAAO,MAAM,gBAAgB,u1EAyE5B,CAAC;AAEF,eAAO,MAAM,kBAAkB,kmDAoD9B,CAAC;AAEF,eAAO,MAAM,qBAAqB,+vJAiIjC,CAAC;AAEF,eAAO,MAAM,6BAA6B,ssHA4GzC,CAAC;AAEF,eAAO,MAAM,8BAA8B,qrTA2O1C,CAAC;AAEF,eAAO,MAAM,wBAAwB,wiPAsMpC,CAAC;AAIF,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUlD,CAAC"}
1
+ {"version":3,"file":"shaders.generated.d.ts","sourceRoot":"","sources":["../../web-driver/shaders.generated.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,0NAQhC,CAAC;AAEF,eAAO,MAAM,yBAAyB,oLAQrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,6oBAwBnC,CAAC;AAEF,eAAO,MAAM,wBAAwB,2QAWpC,CAAC;AAEF,eAAO,MAAM,0BAA0B,kgBAiBtC,CAAC;AAEF,eAAO,MAAM,yBAAyB,2eAiBrC,CAAC;AAEF,eAAO,MAAM,eAAe,q6CAkC3B,CAAC;AAEF,eAAO,MAAM,eAAe,i/HAmH3B,CAAC;AAEF,eAAO,MAAM,eAAe,qgLA6I3B,CAAC;AAEF,eAAO,MAAM,gBAAgB,u1EAyE5B,CAAC;AAEF,eAAO,MAAM,kBAAkB,kmDAoD9B,CAAC;AAEF,eAAO,MAAM,qBAAqB,62KA2IjC,CAAC;AAEF,eAAO,MAAM,6BAA6B,ssHA4GzC,CAAC;AAEF,eAAO,MAAM,8BAA8B,qrTA2O1C,CAAC;AAEF,eAAO,MAAM,wBAAwB,wiPAsMpC,CAAC;AAIF,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUlD,CAAC"}