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.
- package/README.md +30 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/ShadersGenerated.kt +57 -23
- package/catalog/shaders/clouds/clouds.frag +21 -6
- package/catalog/shaders/nebula/nebula.frag +22 -10
- package/catalog/shaders/simianlights/simianlights.frag +19 -9
- package/dist/src/components/preset-control-panel/preset-control-panel.d.ts +9 -1
- package/dist/src/components/preset-control-panel/preset-control-panel.d.ts.map +1 -1
- package/dist/src/components/preset-control-panel/preset-control-panel.js +7 -3
- package/dist/src/components/preset-control-panel/preset-control-panel.js.map +1 -1
- package/dist/src/kaleidoscope/controls.d.ts.map +1 -1
- package/dist/src/kaleidoscope/controls.js +6 -3
- package/dist/src/kaleidoscope/controls.js.map +1 -1
- package/dist/src/kaleidoscope/shader-to-spec.d.ts +15 -1
- package/dist/src/kaleidoscope/shader-to-spec.d.ts.map +1 -1
- package/dist/src/kaleidoscope/shader-to-spec.js +23 -4
- package/dist/src/kaleidoscope/shader-to-spec.js.map +1 -1
- package/dist/src/kaleidoscope/types.d.ts +3 -1
- package/dist/src/kaleidoscope/types.d.ts.map +1 -1
- package/dist/src/kaleidoscope/types.js.map +1 -1
- package/dist/src/persistence/async-storage-store.d.ts +3 -0
- package/dist/src/persistence/async-storage-store.d.ts.map +1 -0
- package/dist/src/persistence/async-storage-store.js +26 -0
- package/dist/src/persistence/async-storage-store.js.map +1 -0
- package/dist/src/persistence/index.d.ts +4 -0
- package/dist/src/persistence/index.d.ts.map +1 -0
- package/dist/src/persistence/index.js +22 -0
- package/dist/src/persistence/index.js.map +1 -0
- package/dist/src/persistence/provider.d.ts +38 -0
- package/dist/src/persistence/provider.d.ts.map +1 -0
- package/dist/src/persistence/provider.js +96 -0
- package/dist/src/persistence/provider.js.map +1 -0
- package/dist/src/persistence/state.d.ts +54 -0
- package/dist/src/persistence/state.d.ts.map +1 -0
- package/dist/src/persistence/state.js +126 -0
- package/dist/src/persistence/state.js.map +1 -0
- package/dist/web-driver/shaders.generated.d.ts +3 -3
- package/dist/web-driver/shaders.generated.d.ts.map +1 -1
- package/dist/web-driver/shaders.generated.js +57 -23
- package/dist/web-driver/shaders.generated.js.map +1 -1
- package/ios/KaleidoscopeModule/shaders/clouds.metalsrc +100 -99
- package/ios/KaleidoscopeModule/shaders/nebula.metalsrc +63 -45
- package/ios/KaleidoscopeModule/shaders/simianlights.metalsrc +63 -45
- package/package.json +14 -2
- package/src/components/preset-control-panel/preset-control-panel.tsx +15 -2
- package/src/kaleidoscope/controls.ts +6 -3
- package/src/kaleidoscope/shader-to-spec.ts +32 -5
- package/src/kaleidoscope/types.ts +3 -1
- package/src/persistence/async-storage-store.ts +33 -0
- package/src/persistence/index.ts +28 -0
- package/src/persistence/provider.tsx +165 -0
- package/src/persistence/state.ts +167 -0
|
@@ -7,10 +7,37 @@
|
|
|
7
7
|
// projection: the layers already carry their own ids, sources, uniforms, and
|
|
8
8
|
// blend. Transforms are not book entries; the transform verb handles them.
|
|
9
9
|
|
|
10
|
-
import type { KaleidoscopePreset } from '../kaleidoscope.preset-book.types';
|
|
10
|
+
import type { KaleidoscopeLayer, KaleidoscopePreset } from '../kaleidoscope.preset-book.types';
|
|
11
11
|
import type { EffectSpec } from './effect.types';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/** The patch wire shape (layer id + partial uniforms), as the verb receives it. */
|
|
14
|
+
type LayerPatchInput = {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly uniforms: Readonly<Record<string, number | readonly number[]>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Project a composite into the spec, optionally merging per-layer uniform
|
|
21
|
+
* patches over the baked values (a switch-with-patches, e.g. restoring a
|
|
22
|
+
* persisted selection). Merging here, at the seam, is what carries the patches
|
|
23
|
+
* to EVERY platform: web rebuilds from these layers, and native re-sends them
|
|
24
|
+
* over setCompositeLayers. A patch addressing a non-tunable or unknown layer id
|
|
25
|
+
* is ignored.
|
|
26
|
+
*/
|
|
27
|
+
export const compositeToEffectSpec = (
|
|
28
|
+
composite: KaleidoscopePreset,
|
|
29
|
+
patches?: ReadonlyArray<LayerPatchInput>,
|
|
30
|
+
): EffectSpec => {
|
|
31
|
+
if (!patches || patches.length === 0) {
|
|
32
|
+
return { name: 'composite', layers: composite.layers };
|
|
33
|
+
}
|
|
34
|
+
const byId = new Map(patches.map((patch) => [patch.id, patch.uniforms]));
|
|
35
|
+
return {
|
|
36
|
+
name: 'composite',
|
|
37
|
+
layers: composite.layers.map((layer) => {
|
|
38
|
+
const override = byId.get(layer.id);
|
|
39
|
+
if (!override || !('uniforms' in layer)) return layer;
|
|
40
|
+
return { ...layer, uniforms: { ...layer.uniforms, ...override } } as KaleidoscopeLayer;
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -78,7 +78,9 @@ export type KaleidoscopeBindOptions<P extends KaleidoscopePresetBook> = {
|
|
|
78
78
|
* The art verb: select a composite by id (rebuilding the pipeline), or clear it
|
|
79
79
|
* with `null`. When `cmd` is the currently-active preset id and `patches` is
|
|
80
80
|
* given, the patches merge through the live no-rebuild uniform channel (keyed by
|
|
81
|
-
* layer id) instead of rebuilding, so a slider drag stays smooth.
|
|
81
|
+
* layer id) instead of rebuilding, so a slider drag stays smooth. On a preset
|
|
82
|
+
* SWITCH, patches merge into the rebuilt layer stack itself, so a restored
|
|
83
|
+
* selection lands tuned on every platform, native included.
|
|
82
84
|
*/
|
|
83
85
|
type KaleidoscopeCommand<P extends KaleidoscopePresetBook> = <K extends keyof P>(
|
|
84
86
|
cmd: K | null,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The default backing store: AsyncStorage under the canonical key. Works on
|
|
2
|
+
// every platform (the web build is localStorage-backed).
|
|
3
|
+
//
|
|
4
|
+
// `@react-native-async-storage/async-storage` is an OPTIONAL peer dependency of
|
|
5
|
+
// the package: only the `/persistence` subpath touches it, and Metro resolves
|
|
6
|
+
// it at bundle time for any app that imports this subpath (even with a custom
|
|
7
|
+
// `store`; bundlers do not tree-shake the default away).
|
|
8
|
+
|
|
9
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
10
|
+
import {
|
|
11
|
+
KALEIDOSCOPE_STATE_KEY,
|
|
12
|
+
type KaleidoscopeStateStore,
|
|
13
|
+
parseStoredKaleidoscopeState,
|
|
14
|
+
type StoredKaleidoscopeState,
|
|
15
|
+
serializeKaleidoscopeState,
|
|
16
|
+
} from './state';
|
|
17
|
+
|
|
18
|
+
export const kaleidoscopeAsyncStorageStore: KaleidoscopeStateStore = {
|
|
19
|
+
load(): Promise<StoredKaleidoscopeState | null> {
|
|
20
|
+
return AsyncStorage.getItem(KALEIDOSCOPE_STATE_KEY).then(
|
|
21
|
+
parseStoredKaleidoscopeState,
|
|
22
|
+
() => null,
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
save(state: StoredKaleidoscopeState): Promise<void> {
|
|
26
|
+
return AsyncStorage.setItem(KALEIDOSCOPE_STATE_KEY, serializeKaleidoscopeState(state)).then(
|
|
27
|
+
() => undefined,
|
|
28
|
+
(error) => {
|
|
29
|
+
console.warn('kaleidoscope: preset persistence save failed', error);
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Subpath entry: react-native-webrtc-kaleidoscope/persistence
|
|
2
|
+
//
|
|
3
|
+
// The persisted-selection convenience: a provider + hook that keep the last
|
|
4
|
+
// applied preset, its control-panel patches, and the mask across launches.
|
|
5
|
+
// Storage-agnostic via `KaleidoscopeStateStore`; defaults to AsyncStorage
|
|
6
|
+
// (`@react-native-async-storage/async-storage`, an optional peer dependency
|
|
7
|
+
// required only by apps that use the default store).
|
|
8
|
+
|
|
9
|
+
export { kaleidoscopeAsyncStorageStore } from './async-storage-store';
|
|
10
|
+
export {
|
|
11
|
+
KaleidoscopeStateProvider,
|
|
12
|
+
type KaleidoscopeStateProviderProps,
|
|
13
|
+
type KaleidoscopeStateValue,
|
|
14
|
+
useKaleidoscopeState,
|
|
15
|
+
} from './provider';
|
|
16
|
+
export {
|
|
17
|
+
DEFAULT_MASK,
|
|
18
|
+
KALEIDOSCOPE_STATE_KEY,
|
|
19
|
+
type KaleidoscopeStateStore,
|
|
20
|
+
parseStoredKaleidoscopeState,
|
|
21
|
+
pruneStoredState,
|
|
22
|
+
type StoredKaleidoscopeState,
|
|
23
|
+
type StoredLayerUniforms,
|
|
24
|
+
type StoredPatch,
|
|
25
|
+
type StoredPatches,
|
|
26
|
+
type StoredPatchMap,
|
|
27
|
+
serializeKaleidoscopeState,
|
|
28
|
+
} from './state';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Persistence: the React surface. `KaleidoscopeStateProvider` hydrates the
|
|
2
|
+
// stored selection once at mount and writes through on every change;
|
|
3
|
+
// `useKaleidoscopeState` hands the host the hydrated values plus the setters.
|
|
4
|
+
//
|
|
5
|
+
// The provider owns STORAGE state only; it never binds a track or calls the
|
|
6
|
+
// verbs. The host applies the restored selection itself (gated on `hydrated`,
|
|
7
|
+
// so a stored preset is not flashed over by the default):
|
|
8
|
+
//
|
|
9
|
+
// const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();
|
|
10
|
+
// useEffect(() => {
|
|
11
|
+
// if (!hydrated || !controls) return;
|
|
12
|
+
// if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));
|
|
13
|
+
// else controls.kaleidoscope(null);
|
|
14
|
+
// }, [hydrated, controls, presetId]);
|
|
15
|
+
//
|
|
16
|
+
// The backing store defaults to AsyncStorage; pass any `KaleidoscopeStateStore`
|
|
17
|
+
// to swap it. Importing this subpath is what brings the optional
|
|
18
|
+
// `@react-native-async-storage/async-storage` peer onto your bundle path
|
|
19
|
+
// (Metro resolves it at bundle time either way, so there is no lazy escape).
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createContext,
|
|
23
|
+
type ReactElement,
|
|
24
|
+
type ReactNode,
|
|
25
|
+
useContext,
|
|
26
|
+
useEffect,
|
|
27
|
+
useRef,
|
|
28
|
+
useState,
|
|
29
|
+
} from 'react';
|
|
30
|
+
import type { MaskInput, PatchesFor } from '../kaleidoscope/types';
|
|
31
|
+
import type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';
|
|
32
|
+
import { kaleidoscopeAsyncStorageStore } from './async-storage-store';
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_MASK,
|
|
35
|
+
type KaleidoscopeStateStore,
|
|
36
|
+
mergePatch,
|
|
37
|
+
patchListFor,
|
|
38
|
+
pruneStoredState,
|
|
39
|
+
type StoredPatch,
|
|
40
|
+
type StoredPatches,
|
|
41
|
+
} from './state';
|
|
42
|
+
|
|
43
|
+
export type KaleidoscopeStateValue<P extends KaleidoscopePresetBook = KaleidoscopePresetBook> = {
|
|
44
|
+
/** False until the persisted selection has been read; apply no effects before then. */
|
|
45
|
+
readonly hydrated: boolean;
|
|
46
|
+
/** The selected preset id, or null when nothing is selected. */
|
|
47
|
+
readonly presetId: (keyof P & string) | null;
|
|
48
|
+
/** The shared segmentation edge. */
|
|
49
|
+
readonly mask: MaskInput;
|
|
50
|
+
/** Every preset's stored per-layer overrides (keyed by preset id, then layer id). */
|
|
51
|
+
readonly patches: StoredPatches;
|
|
52
|
+
readonly setPreset: (presetId: (keyof P & string) | null) => void;
|
|
53
|
+
readonly setMask: (mask: MaskInput) => void;
|
|
54
|
+
/** Record one control-panel patch against a preset (and persist it). */
|
|
55
|
+
readonly setPatch: (presetId: keyof P & string, patch: StoredPatch) => void;
|
|
56
|
+
/** A preset's stored overrides in the array shape `kaleidoscope(id, patches)` takes. */
|
|
57
|
+
readonly patchesFor: <K extends keyof P & string>(presetId: K) => PatchesFor<P, K>;
|
|
58
|
+
/** Clear the stored selection back to defaults (and persist the cleared state). */
|
|
59
|
+
readonly reset: () => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const KaleidoscopeStateContext = createContext<KaleidoscopeStateValue | null>(null);
|
|
63
|
+
|
|
64
|
+
export type KaleidoscopeStateProviderProps<P extends KaleidoscopePresetBook> = {
|
|
65
|
+
/** The consumer's preset book; stored state is pruned against it at hydrate. */
|
|
66
|
+
readonly presets: P;
|
|
67
|
+
/** The backing store. Defaults to the AsyncStorage store (lazily loaded). */
|
|
68
|
+
readonly store?: KaleidoscopeStateStore;
|
|
69
|
+
/** The mask used before hydration and after `reset`. Defaults to 0.5/0.5. */
|
|
70
|
+
readonly defaultMask?: MaskInput;
|
|
71
|
+
readonly children: ReactNode;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type Selection = {
|
|
75
|
+
readonly presetId: string | null;
|
|
76
|
+
readonly mask: MaskInput;
|
|
77
|
+
readonly patches: StoredPatches;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function KaleidoscopeStateProvider<P extends KaleidoscopePresetBook>({
|
|
81
|
+
presets,
|
|
82
|
+
store,
|
|
83
|
+
defaultMask = DEFAULT_MASK,
|
|
84
|
+
children,
|
|
85
|
+
}: KaleidoscopeStateProviderProps<P>): ReactElement {
|
|
86
|
+
const [hydrated, setHydrated] = useState(false);
|
|
87
|
+
const [selection, setSelection] = useState<Selection>({
|
|
88
|
+
presetId: null,
|
|
89
|
+
mask: defaultMask,
|
|
90
|
+
patches: {},
|
|
91
|
+
});
|
|
92
|
+
// A write before hydration wins over the stored value (the person acted; do
|
|
93
|
+
// not clobber their fresh choice with yesterday's).
|
|
94
|
+
const dirty = useRef(false);
|
|
95
|
+
// Pinned for the provider's lifetime: hydrate and every write-through go to
|
|
96
|
+
// the same store the first render saw.
|
|
97
|
+
const storeRef = useRef(store ?? kaleidoscopeAsyncStorageStore);
|
|
98
|
+
|
|
99
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: hydrate exactly once; the book and store are bind-time constants.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
let cancelled = false;
|
|
102
|
+
storeRef.current.load().then(
|
|
103
|
+
(stored) => {
|
|
104
|
+
if (cancelled) return;
|
|
105
|
+
if (stored && !dirty.current) {
|
|
106
|
+
const pruned = pruneStoredState(stored, presets);
|
|
107
|
+
setSelection({ presetId: pruned.presetId, mask: pruned.mask, patches: pruned.patches });
|
|
108
|
+
}
|
|
109
|
+
setHydrated(true);
|
|
110
|
+
},
|
|
111
|
+
() => {
|
|
112
|
+
if (!cancelled) setHydrated(true);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
return () => {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const commit = (next: Selection): void => {
|
|
121
|
+
dirty.current = true;
|
|
122
|
+
setSelection(next);
|
|
123
|
+
void storeRef.current.save({ version: 1, ...next });
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const value: KaleidoscopeStateValue = {
|
|
127
|
+
hydrated,
|
|
128
|
+
presetId: selection.presetId,
|
|
129
|
+
mask: selection.mask,
|
|
130
|
+
patches: selection.patches,
|
|
131
|
+
setPreset: (presetId) => commit({ ...selection, presetId }),
|
|
132
|
+
setMask: (mask) => commit({ ...selection, mask }),
|
|
133
|
+
setPatch: (presetId, patch) =>
|
|
134
|
+
commit({ ...selection, patches: mergePatch(selection.patches, presetId, patch) }),
|
|
135
|
+
// The stored wire shape is book-agnostic; the typed view is recovered at the
|
|
136
|
+
// hook (`useKaleidoscopeState<typeof presets>()`), so the cast is the seam.
|
|
137
|
+
patchesFor: ((presetId: string) =>
|
|
138
|
+
patchListFor(selection.patches, presetId)) as KaleidoscopeStateValue['patchesFor'],
|
|
139
|
+
reset: () => commit({ presetId: null, mask: defaultMask, patches: {} }),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<KaleidoscopeStateContext.Provider value={value}>{children}</KaleidoscopeStateContext.Provider>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The persisted selection plus its setters. Typed by the consumer's book:
|
|
149
|
+
* `useKaleidoscopeState<typeof presets>()`. Throws outside the provider.
|
|
150
|
+
*/
|
|
151
|
+
export function useKaleidoscopeState<
|
|
152
|
+
P extends KaleidoscopePresetBook = KaleidoscopePresetBook,
|
|
153
|
+
>(): KaleidoscopeStateValue<P> {
|
|
154
|
+
const value = useContext(KaleidoscopeStateContext);
|
|
155
|
+
if (value === null) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
'useKaleidoscopeState: no <KaleidoscopeStateProvider> above this component. ' +
|
|
158
|
+
'Wrap your app (or the screen using the picker) in the provider from ' +
|
|
159
|
+
"'react-native-webrtc-kaleidoscope/persistence'.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
// Safe by construction: the provider pruned ids against the same book the
|
|
163
|
+
// consumer parameterizes with; the runtime shapes are identical.
|
|
164
|
+
return value as unknown as KaleidoscopeStateValue<P>;
|
|
165
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Persistence: the pure state module. The stored shape, its key, the tolerant
|
|
2
|
+
// parse, and the pure helpers the provider uses to prune, merge, and project
|
|
3
|
+
// stored state. No React, no storage; the store interface is the only boundary
|
|
4
|
+
// (a consumer may back it with AsyncStorage, localStorage, MMKV, anything
|
|
5
|
+
// promise-shaped). Everything here is unit-testable in plain Node.
|
|
6
|
+
//
|
|
7
|
+
// What persists is the person's selection: the preset id they last applied, the
|
|
8
|
+
// per-layer uniform patches they dialed in through the control panels (keyed by
|
|
9
|
+
// preset, so tweaks to several presets all survive), and the shared mask edge.
|
|
10
|
+
|
|
11
|
+
import type { MaskInput } from '../kaleidoscope/types';
|
|
12
|
+
import type { KaleidoscopePresetBook } from '../kaleidoscope.preset-book.types';
|
|
13
|
+
|
|
14
|
+
/** One layer's stored uniform overrides (the wire shape `onPatch` emits). */
|
|
15
|
+
export type StoredLayerUniforms = Readonly<Record<string, number | readonly number[]>>;
|
|
16
|
+
|
|
17
|
+
/** A preset's stored overrides, keyed by layer id. */
|
|
18
|
+
export type StoredPatchMap = Readonly<Record<string, StoredLayerUniforms>>;
|
|
19
|
+
|
|
20
|
+
/** Every preset's stored overrides, keyed by preset id. */
|
|
21
|
+
export type StoredPatches = Readonly<Record<string, StoredPatchMap>>;
|
|
22
|
+
|
|
23
|
+
/** The single live-patch shape, identical to `KaleidoscopeControls['onPatch']`'s argument. */
|
|
24
|
+
export type StoredPatch = {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly uniforms: StoredLayerUniforms;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type StoredKaleidoscopeState = {
|
|
30
|
+
readonly version: 1;
|
|
31
|
+
/** The last-applied preset id, or null when nothing was selected. */
|
|
32
|
+
readonly presetId: string | null;
|
|
33
|
+
/** The shared segmentation edge. */
|
|
34
|
+
readonly mask: MaskInput;
|
|
35
|
+
/** Per-preset, per-layer uniform overrides from the control panels. */
|
|
36
|
+
readonly patches: StoredPatches;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The backing store. `load` resolves null when nothing (or nothing readable) is
|
|
41
|
+
* stored; `save` swallows its own failures (persistence is a convenience, never
|
|
42
|
+
* a crash).
|
|
43
|
+
*/
|
|
44
|
+
export interface KaleidoscopeStateStore {
|
|
45
|
+
load(): Promise<StoredKaleidoscopeState | null>;
|
|
46
|
+
save(state: StoredKaleidoscopeState): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const KALEIDOSCOPE_STATE_KEY = 'kaleidoscope.state.v1';
|
|
50
|
+
|
|
51
|
+
export const DEFAULT_MASK: MaskInput = { hardness: 0.5, threshold: 0.5 };
|
|
52
|
+
|
|
53
|
+
export const serializeKaleidoscopeState = (state: StoredKaleidoscopeState): string =>
|
|
54
|
+
JSON.stringify(state);
|
|
55
|
+
|
|
56
|
+
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
|
|
57
|
+
|
|
58
|
+
const isUniformValue = (value: unknown): value is number | readonly number[] =>
|
|
59
|
+
typeof value === 'number' ||
|
|
60
|
+
(Array.isArray(value) && value.every((entry) => typeof entry === 'number'));
|
|
61
|
+
|
|
62
|
+
// A patch map (layer id -> uniforms) with every malformed entry dropped.
|
|
63
|
+
const parsePatchMap = (raw: unknown): StoredPatchMap | null => {
|
|
64
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return null;
|
|
65
|
+
const out: Record<string, StoredLayerUniforms> = {};
|
|
66
|
+
for (const [layerId, uniforms] of Object.entries(raw)) {
|
|
67
|
+
if (typeof uniforms !== 'object' || uniforms === null || Array.isArray(uniforms)) continue;
|
|
68
|
+
const kept: Record<string, number | readonly number[]> = {};
|
|
69
|
+
for (const [key, value] of Object.entries(uniforms)) {
|
|
70
|
+
if (isUniformValue(value)) kept[key] = value;
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(kept).length > 0) out[layerId] = kept;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Tolerant parse: any malformed or wrong-version payload reads as null, and a
|
|
79
|
+
* malformed `patches` subtree degrades to the valid subset rather than killing
|
|
80
|
+
* the whole state. Mask values clamp to 0..1 (the verbs' documented range).
|
|
81
|
+
*/
|
|
82
|
+
export const parseStoredKaleidoscopeState = (
|
|
83
|
+
raw: string | null,
|
|
84
|
+
): StoredKaleidoscopeState | null => {
|
|
85
|
+
if (!raw) return null;
|
|
86
|
+
let parsed: unknown;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(raw);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (typeof parsed !== 'object' || parsed === null) return null;
|
|
93
|
+
const candidate = parsed as Record<string, unknown>;
|
|
94
|
+
if (candidate.version !== 1) return null;
|
|
95
|
+
if (candidate.presetId !== null && typeof candidate.presetId !== 'string') return null;
|
|
96
|
+
const mask = candidate.mask as Record<string, unknown> | null | undefined;
|
|
97
|
+
if (!mask || typeof mask.hardness !== 'number' || typeof mask.threshold !== 'number') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const patches: Record<string, StoredPatchMap> = {};
|
|
101
|
+
if (typeof candidate.patches === 'object' && candidate.patches !== null) {
|
|
102
|
+
for (const [presetId, rawMap] of Object.entries(candidate.patches)) {
|
|
103
|
+
const map = parsePatchMap(rawMap);
|
|
104
|
+
if (map && Object.keys(map).length > 0) patches[presetId] = map;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
version: 1,
|
|
109
|
+
presetId: candidate.presetId as string | null,
|
|
110
|
+
mask: { hardness: clamp01(mask.hardness), threshold: clamp01(mask.threshold) },
|
|
111
|
+
patches,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// The layer ids a preset can be patched on: the layers that carry uniforms
|
|
116
|
+
// (`image` and `direct` layers have none and cannot be patched).
|
|
117
|
+
const tunableLayerIds = (book: KaleidoscopePresetBook, presetId: string): ReadonlySet<string> => {
|
|
118
|
+
const preset = book[presetId];
|
|
119
|
+
if (!preset) return new Set();
|
|
120
|
+
return new Set(preset.layers.filter((layer) => 'uniforms' in layer).map((layer) => layer.id));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reconcile stored state against the consumer's current book: a preset that no
|
|
125
|
+
* longer exists reads as "none", a patch for a vanished preset or layer is
|
|
126
|
+
* dropped. Stale state degrades silently; it never crashes the picker.
|
|
127
|
+
*/
|
|
128
|
+
export const pruneStoredState = (
|
|
129
|
+
state: StoredKaleidoscopeState,
|
|
130
|
+
book: KaleidoscopePresetBook,
|
|
131
|
+
): StoredKaleidoscopeState => {
|
|
132
|
+
const presetId = state.presetId !== null && state.presetId in book ? state.presetId : null;
|
|
133
|
+
const patches: Record<string, StoredPatchMap> = {};
|
|
134
|
+
for (const [id, map] of Object.entries(state.patches)) {
|
|
135
|
+
if (!(id in book)) continue;
|
|
136
|
+
const tunable = tunableLayerIds(book, id);
|
|
137
|
+
const kept: Record<string, StoredLayerUniforms> = {};
|
|
138
|
+
for (const [layerId, uniforms] of Object.entries(map)) {
|
|
139
|
+
if (tunable.has(layerId)) kept[layerId] = uniforms;
|
|
140
|
+
}
|
|
141
|
+
if (Object.keys(kept).length > 0) patches[id] = kept;
|
|
142
|
+
}
|
|
143
|
+
return { version: 1, presetId, mask: state.mask, patches };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Merge one live patch into a preset's stored overrides (immutably). */
|
|
147
|
+
export const mergePatch = (
|
|
148
|
+
patches: StoredPatches,
|
|
149
|
+
presetId: string,
|
|
150
|
+
patch: StoredPatch,
|
|
151
|
+
): StoredPatches => ({
|
|
152
|
+
...patches,
|
|
153
|
+
[presetId]: {
|
|
154
|
+
...patches[presetId],
|
|
155
|
+
[patch.id]: { ...patches[presetId]?.[patch.id], ...patch.uniforms },
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Project a preset's stored overrides into the array shape the `kaleidoscope`
|
|
161
|
+
* verb takes as `patches`.
|
|
162
|
+
*/
|
|
163
|
+
export const patchListFor = (
|
|
164
|
+
patches: StoredPatches,
|
|
165
|
+
presetId: string,
|
|
166
|
+
): ReadonlyArray<StoredPatch> =>
|
|
167
|
+
Object.entries(patches[presetId] ?? {}).map(([id, uniforms]) => ({ id, uniforms }));
|