react-native-webrtc-kaleidoscope 2.7.5 → 2.7.7

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.
@@ -1 +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"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/persistence/provider.tsx"],"names":[],"mappings":"AAuBA,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"}
@@ -11,9 +11,12 @@ const jsx_runtime_1 = require("react/jsx-runtime");
11
11
  // verbs. The host applies the restored selection itself (gated on `hydrated`,
12
12
  // so a stored preset is not flashed over by the default):
13
13
  //
14
+ // // `controls` is the binding from bindKaleidoscope(track, { presets }).
14
15
  // const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();
15
16
  // useEffect(() => {
16
17
  // if (!hydrated || !controls) return;
18
+ // // patchesFor reads this render's patches; live edits go through onPatch,
19
+ // // not this re-apply, so it stays out of the deps.
17
20
  // if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));
18
21
  // else controls.kaleidoscope(null);
19
22
  // }, [hydrated, controls, presetId]);
@@ -1 +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"]}
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// // `controls` is the binding from bindKaleidoscope(track, { presets }).\n// const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();\n// useEffect(() => {\n// if (!hydrated || !controls) return;\n// // patchesFor reads this render's patches; live edits go through onPatch,\n// // not this re-apply, so it stays out of the deps.\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-webrtc-kaleidoscope",
3
- "version": "2.7.5",
3
+ "version": "2.7.7",
4
4
  "description": "Live video effects (blur, background replacement, generative backgrounds, flip/rotate) for react-native-webrtc, packaged as a managed-Expo-friendly Expo Module. Working on web, Android, and iOS. Active development.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -538,15 +538,11 @@
538
538
  "./expo-module.config.json": "./expo-module.config.json",
539
539
  "./package.json": "./package.json"
540
540
  },
541
- "bin": {
542
- "kaleidoscope-thumbnails": "./tools/thumbnails/make-thumbnails.ts"
543
- },
544
541
  "files": [
545
542
  "src",
546
543
  "catalog",
547
544
  "dist",
548
- "tools",
549
- "android/src",
545
+ "android/src/main",
550
546
  "android/build.gradle",
551
547
  "ios",
552
548
  "app.plugin.js",
@@ -16,10 +16,14 @@ exports.bumpDeploymentTarget = bumpDeploymentTarget;
16
16
  const node_fs_1 = __importDefault(require('node:fs'));
17
17
  const node_path_1 = __importDefault(require('node:path'));
18
18
  const constants_1 = require('../lib/constants');
19
- // Mirrors ios/Kaleidoscope.podspec :ios => '15.0'. Bumping the podspec requires
20
- // bumping this constant; the drift cost is a silent pod-install drop (the bug
21
- // this plugin fixes): expo-modules-autolinking drops any pod whose declared
22
- // minimum platform exceeds the Podfile platform.
19
+ // The Kaleidoscope POD's own iOS floor; mirrors ios/Kaleidoscope.podspec
20
+ // :ios => '15.0'. Bumping the podspec requires bumping this constant. It is
21
+ // reconciled against the HOST RN's floor at write time (see bumpDeploymentTarget):
22
+ // the pod floor is only forced when it EXCEEDS the floor the generated Podfile
23
+ // already defaults to, because expo-modules-autolinking drops any pod whose
24
+ // declared minimum platform exceeds the Podfile platform. It is never written
25
+ // BELOW the Podfile's own default, which would lower RN's floor and fail pod
26
+ // install on RN's core pods (issue #86: RN >= 0.81's floor 15.1 exceeds this).
23
27
  const IOS_DEPLOYMENT_TARGET = '15.0';
24
28
  /**
25
29
  * Compare two dotted version strings element-wise. Returns true iff `existing`
@@ -42,15 +46,62 @@ function isVersionLessThan(existing, target) {
42
46
  return false; // equal
43
47
  }
44
48
  /**
45
- * Raise `ios.deploymentTarget` in Podfile.properties.json to the library
46
- * minimum. The generated Podfile reads `podfile_properties['ios.deploymentTarget']`
47
- * at the top, so this is the canonical, no-Podfile-edit override seam. Without
48
- * this bump the Kaleidoscope pod is silently dropped and the runtime module
49
- * lookup throws. Non-downgrading, idempotent, non-fatal (warn, never throw).
49
+ * The iOS deployment-target FLOOR the generated Podfile falls back to when
50
+ * `ios.deploymentTarget` is unset. Expo/RN prebuild bakes the host RN's own floor
51
+ * (`min_ios_version_supported`) into the Podfile's platform line as a literal,
52
+ * e.g. `platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'` (or a
53
+ * bare `platform :ios, '15.1'`). That literal is exactly the value the Podfile
54
+ * uses when we leave the props key alone, so it is the floor we must not drop
55
+ * below. Returns the dotted version, or undefined if the Podfile is absent,
56
+ * unreadable, or states its fallback as a non-literal (a Ruby call) we cannot read
57
+ * statically; the caller then conservatively forces the pod floor (prior behavior).
50
58
  *
51
- * Reads the file by hand (not readTextOrNull) because it must DISTINGUISH a
52
- * missing file (treat as `{}` and write the bump) from an unreadable one (bail
53
- * without clobbering); a distinction readTextOrNull deliberately flattens.
59
+ * The Podfile, already on disk at mod-execution time, is preferred over reaching
60
+ * into react-native's Ruby helper: no dependency to resolve, no coupling to RN's
61
+ * internal file layout or `exports` map, and it reflects any consumer Podfile
62
+ * override too.
63
+ */
64
+ function readPodfileIosFloor(platformProjectRoot) {
65
+ try {
66
+ const podfile = node_fs_1.default.readFileSync(
67
+ node_path_1.default.join(platformProjectRoot, 'Podfile'),
68
+ 'utf8',
69
+ );
70
+ const platformLine = podfile.match(/^[ \t]*platform[ \t]+:ios\b.*$/m)?.[0];
71
+ if (!platformLine) return undefined;
72
+ // The only dotted-numeric quoted token on the platform line is the floor
73
+ // literal; `'ios.deploymentTarget'` is non-numeric. Take the last match so a
74
+ // bare `platform :ios, 'X.Y'` and the `|| 'X.Y'` fallback form both resolve.
75
+ const versions = platformLine.match(/['"]\d+\.\d+(?:\.\d+)?['"]/g);
76
+ const last = versions?.[versions.length - 1];
77
+ return last ? last.replace(/['"]/g, '') : undefined;
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ /**
83
+ * Reconcile `ios.deploymentTarget` in Podfile.properties.json so the Kaleidoscope
84
+ * pod is not dropped by expo-modules-autolinking (which drops any pod whose
85
+ * declared minimum platform exceeds the Podfile platform), WITHOUT ever lowering
86
+ * the host RN's own floor. The generated Podfile reads
87
+ * `podfile_properties['ios.deploymentTarget']` at the top, so this props key is the
88
+ * canonical, no-Podfile-edit override seam.
89
+ *
90
+ * Given the pod floor (IOS_DEPLOYMENT_TARGET) and the Podfile's own fallback floor
91
+ * (readPodfileIosFloor, i.e. the host RN's floor):
92
+ * - key set >= pod floor: leave it (never downgrade).
93
+ * - key set < pod floor: raise it to the pod floor.
94
+ * - key unset, Podfile floor already >= pod floor: DEFER (leave unset). The
95
+ * Podfile's `|| <floor>` already satisfies the pod, so writing the lower pod
96
+ * floor would clobber it and break pod install on RN's core pods (issue #86).
97
+ * Deferring also auto-tracks future RN floor bumps with no code change.
98
+ * - key unset, Podfile floor below or unknown: force the pod floor (prior
99
+ * behavior, e.g. RN 0.74's 13.4 < 15.0).
100
+ * Idempotent, non-fatal (warn, never throw).
101
+ *
102
+ * Reads the props file by hand (not readTextOrNull) because it must DISTINGUISH a
103
+ * missing file (treat as `{}`) from an unreadable one (bail without clobbering);
104
+ * a distinction readTextOrNull deliberately flattens.
54
105
  */
55
106
  function bumpDeploymentTarget(platformProjectRoot) {
56
107
  const propsPath = node_path_1.default.join(platformProjectRoot, 'Podfile.properties.json');
@@ -90,10 +141,20 @@ function bumpDeploymentTarget(platformProjectRoot) {
90
141
  for (const key of Object.keys(parsed)) {
91
142
  next[key] = parsed[key];
92
143
  }
93
- const existing = next['ios.deploymentTarget'];
94
- if (
95
- isVersionLessThan(typeof existing === 'string' ? existing : undefined, IOS_DEPLOYMENT_TARGET)
96
- ) {
144
+ const existingRaw = next['ios.deploymentTarget'];
145
+ const existing = typeof existingRaw === 'string' ? existingRaw : undefined;
146
+ if (existing === undefined) {
147
+ // Unset: the Podfile resolves `|| <floor literal>`, which tracks the host
148
+ // RN's own iOS floor. Only force the pod floor when that fallback floor is
149
+ // below it (or unreadable); otherwise defer so we never lower RN's floor.
150
+ // `isVersionLessThan(undefined, ...)` is true, so an unreadable Podfile floor
151
+ // falls back to forcing the pod floor (prior behavior).
152
+ const podfileFloor = readPodfileIosFloor(platformProjectRoot);
153
+ if (isVersionLessThan(podfileFloor, IOS_DEPLOYMENT_TARGET)) {
154
+ next['ios.deploymentTarget'] = IOS_DEPLOYMENT_TARGET;
155
+ }
156
+ } else if (isVersionLessThan(existing, IOS_DEPLOYMENT_TARGET)) {
157
+ // Explicit value below the pod floor: raise it. At or above: leave it.
97
158
  next['ios.deploymentTarget'] = IOS_DEPLOYMENT_TARGET;
98
159
  }
99
160
  node_fs_1.default.writeFileSync(propsPath, `${JSON.stringify(next, null, 2)}\n`);
package/src/index.ts CHANGED
@@ -192,7 +192,7 @@ const specToNativeName = (spec: ReturnType<typeof toEffectSpec>): string => {
192
192
  const lastAppliedSignatureByTrack = new WeakMap<object, string>();
193
193
 
194
194
  // The lower-level native primitive: route a spec array through the upstream
195
- // `_setVideoEffects`. Internal now (the public surface is the three verbs);
195
+ // `_setVideoEffects`. Internal now (the public surface is the four verbs);
196
196
  // `bindKaleidoscope`'s reconcile drives it.
197
197
  const applyVideoEffects: ApplyVideoEffects = (track, effects) => {
198
198
  const t = track as MediaStreamTrack & WebRTCTrackExtensions;
@@ -264,8 +264,8 @@ const applyVideoEffects: ApplyVideoEffects = (track, effects) => {
264
264
  };
265
265
 
266
266
  /**
267
- * Bind a track and a preset book; get the three verbs back
268
- * (`{ kaleidoscope, transform, mask }`). On native the track is mutated in
267
+ * Bind a track and a preset book; get the four verbs back
268
+ * (`{ kaleidoscope, transform, mask, dispose }`). On native the track is mutated in
269
269
  * place, so `controls.track` is the bound track and `onTrack` fires with it
270
270
  * after each `kaleidoscope` preset switch and `transform` command. `mask`
271
271
  * updates the segmentation edge the per-frame processors read.
package/src/index.web.ts CHANGED
@@ -152,8 +152,8 @@ export const applyVideoEffectsDisposable = (
152
152
  };
153
153
 
154
154
  /**
155
- * Bind a track and a preset book; get the three verbs back
156
- * (`{ kaleidoscope, transform, mask }`). Presets live in the consumer's
155
+ * Bind a track and a preset book; get the four verbs back
156
+ * (`{ kaleidoscope, transform, mask, dispose }`). Presets live in the consumer's
157
157
  * project; these verbs drive them. On web each `kaleidoscope` preset switch and
158
158
  * each `transform` command rebuilds the Insertable-Streams pipeline and yields a
159
159
  * new output track, so read the live track from `onTrack` (or `controls.track`);
@@ -1,4 +1,4 @@
1
- // The three-verb controls: shared composite-state machine, platform-agnostic.
1
+ // The four-verb controls: shared composite-state machine, platform-agnostic.
2
2
  //
3
3
  // Holds the art effect (one composite) and the transform op list, reconciles
4
4
  // them into an ordered EffectSpec array (art FIRST so segmentation sees the
@@ -1,6 +1,6 @@
1
- // The three-verb surface: types.
1
+ // The four-verb surface: types.
2
2
  //
3
- // Bind a track and a preset book once; get three typed verbs back:
3
+ // Bind a track and a preset book once; get four typed verbs back:
4
4
  // - kaleidoscope(cmd, patches?) the art axis: which composite (layer stack)
5
5
  // fills the frame. cmd is a preset id from the book (narrowed), or null to
6
6
  // clear. patches optionally merge per-layer uniform overrides (addressed by
@@ -8,6 +8,7 @@
8
8
  // no-rebuild channel, so sliders stay smooth.
9
9
  // - transform(t?) the geometry axis: absolute flips + 90° rotation.
10
10
  // - mask(m) the segmentation edge shared by every art effect.
11
+ // - dispose() tear down the pipeline; release the bound track.
11
12
  //
12
13
  // Shaders live in the library; consumers add presets (composites) over them,
13
14
  // never new shaders. Per shader-world convention, numeric uniforms are
@@ -88,7 +89,7 @@ type KaleidoscopeCommand<P extends KaleidoscopePresetBook> = <K extends keyof P>
88
89
  ) => void;
89
90
 
90
91
  /**
91
- * The three verbs for one bound track and book, plus the live track and a
92
+ * The four verbs for one bound track and book, plus the live track and a
92
93
  * teardown. `kaleidoscope` (preset switch) and `transform` rebuild the composite
93
94
  * (web yields a new track via onTrack); a `kaleidoscope` patch of the active
94
95
  * preset and `mask` both update what the running composite reads each frame, so
@@ -6,9 +6,12 @@
6
6
  // verbs. The host applies the restored selection itself (gated on `hydrated`,
7
7
  // so a stored preset is not flashed over by the default):
8
8
  //
9
+ // // `controls` is the binding from bindKaleidoscope(track, { presets }).
9
10
  // const { hydrated, presetId, patchesFor, mask, ... } = useKaleidoscopeState<typeof presets>();
10
11
  // useEffect(() => {
11
12
  // if (!hydrated || !controls) return;
13
+ // // patchesFor reads this render's patches; live edits go through onPatch,
14
+ // // not this re-apply, so it stays out of the deps.
12
15
  // if (presetId) controls.kaleidoscope(presetId, patchesFor(presetId));
13
16
  // else controls.kaleidoscope(null);
14
17
  // }, [hydrated, controls, presetId]);
@@ -1,165 +0,0 @@
1
- package com.simiancraft.kaleidoscope
2
-
3
- import org.junit.Assert.assertArrayEquals
4
- import org.junit.Assert.assertEquals
5
- import org.junit.Assert.assertNull
6
- import org.junit.Assert.assertTrue
7
- import org.junit.Before
8
- import org.junit.Test
9
-
10
- /**
11
- * Unit tests for the composite layer-stack wire contract: the JSON that
12
- * serializeCompositeLayers (src/index.ts) sends across the bridge and that
13
- * CompositeLayers.set/get/clear parses into the snapshot the compositor reads each
14
- * frame. These pin the parse rules the iOS CompositeLayers.swift port must match;
15
- * the same cases are mirrored in the iOS XCTest suite, so a divergence in either
16
- * parser fails on one side.
17
- *
18
- * Runs as a JVM unit test (testDebugUnitTest): the real org.json is on the test
19
- * classpath and android.util.Log is a no-op stub (unitTests.returnDefaultValues).
20
- */
21
- class CompositeLayersTest {
22
- // CompositeLayers is a process-global singleton; reset between cases so a prior
23
- // set() can't leak into the next assertion.
24
- @Before
25
- fun reset() = CompositeLayers.clear()
26
-
27
- // The wizard-tower composite exactly as serializeCompositeLayers emits it: a
28
- // generative base (clouds, uniforms), the cut-out image (image, source = the
29
- // stable image id), then you (direct, subject). The canonical happy path.
30
- @Test
31
- fun parsesWizardTowerStack() {
32
- CompositeLayers.set(
33
- """
34
- [
35
- {"id":"sky","shader":"clouds","target":"background","uniforms":{"uExposure":1.26,"uSkyLowColor":[0.99,0.62,0.03],"uCloudSpeed":0.92}},
36
- {"id":"tower","shader":"image","target":"background","source":"wizards-tower"},
37
- {"id":"you","shader":"direct","target":"subject"}
38
- ]
39
- """.trimIndent(),
40
- )
41
- val layers = CompositeLayers.get()
42
- assertEquals(3, layers.size)
43
-
44
- val clouds = layers[0]
45
- assertEquals("sky", clouds.id)
46
- assertEquals("clouds", clouds.shader)
47
- assertEquals("background", clouds.target)
48
- assertNull(clouds.blend)
49
- assertNull(clouds.source)
50
- assertArrayEquals(floatArrayOf(1.26f), clouds.uniforms["uExposure"], EPS)
51
- assertArrayEquals(floatArrayOf(0.99f, 0.62f, 0.03f), clouds.uniforms["uSkyLowColor"], EPS)
52
-
53
- val image = layers[1]
54
- assertEquals("tower", image.id)
55
- assertEquals("image", image.shader)
56
- assertEquals("wizards-tower", image.source)
57
- assertTrue(image.uniforms.isEmpty())
58
-
59
- val you = layers[2]
60
- assertEquals("you", you.id)
61
- assertEquals("direct", you.shader)
62
- assertEquals("subject", you.target)
63
- assertNull(you.source)
64
- }
65
-
66
- // An overlay layer carries a blend mode; additive must round-trip, and the
67
- // base layer (no blend key) must stay null (the compositor reads null = opaque).
68
- @Test
69
- fun parsesAdditiveBlendOverOpaqueBase() {
70
- CompositeLayers.set(
71
- """
72
- [
73
- {"id":"image","shader":"image","target":"background","source":"stylized-dark"},
74
- {"id":"rays","shader":"godrays","target":"background","blend":"additive","uniforms":{"uRayCount":11}}
75
- ]
76
- """.trimIndent(),
77
- )
78
- val layers = CompositeLayers.get()
79
- assertEquals(2, layers.size)
80
- assertEquals("image", layers[0].id)
81
- assertNull(layers[0].blend)
82
- assertEquals("rays", layers[1].id)
83
- assertEquals("additive", layers[1].blend)
84
- assertArrayEquals(floatArrayOf(11f), layers[1].uniforms["uRayCount"], EPS)
85
- }
86
-
87
- // `id` is always on the wire now; a payload missing it falls back to the array
88
- // index so the address stays stable and unique-per-stack rather than colliding.
89
- @Test
90
- fun fallsBackToIndexWhenIdMissing() {
91
- CompositeLayers.set(
92
- """[{"shader":"clouds","uniforms":{}},{"shader":"direct","target":"subject"}]""",
93
- )
94
- val layers = CompositeLayers.get()
95
- assertEquals(2, layers.size)
96
- assertEquals("0", layers[0].id)
97
- assertEquals("1", layers[1].id)
98
- }
99
-
100
- // target defaults to "background" when the wire omits it.
101
- @Test
102
- fun defaultsMissingTargetToBackground() {
103
- CompositeLayers.set("""[{"shader":"clouds","uniforms":{}}]""")
104
- assertEquals("background", CompositeLayers.get()[0].target)
105
- }
106
-
107
- // A scalar uniform normalizes to a one-element array; a numeric array stays a
108
- // vector, in order.
109
- @Test
110
- fun normalizesScalarAndVectorUniforms() {
111
- CompositeLayers.set("""[{"shader":"plasma","uniforms":{"speed":0.3,"colorA":[0.0,0.3,0.6]}}]""")
112
- val u = CompositeLayers.get()[0].uniforms
113
- assertArrayEquals(floatArrayOf(0.3f), u["speed"], EPS)
114
- assertArrayEquals(floatArrayOf(0.0f, 0.3f, 0.6f), u["colorA"], EPS)
115
- }
116
-
117
- // A uniform whose value is neither a number nor a numeric array is skipped;
118
- // the shader keeps its GLSL default for that name. Well-formed siblings stay.
119
- @Test
120
- fun skipsNonNumericUniformKeepingSiblings() {
121
- CompositeLayers.set("""[{"shader":"plasma","uniforms":{"speed":0.3,"bad":"nope"}}]""")
122
- val u = CompositeLayers.get()[0].uniforms
123
- assertArrayEquals(floatArrayOf(0.3f), u["speed"], EPS)
124
- assertNull(u["bad"])
125
- }
126
-
127
- // A layer with no shader is skipped; well-formed siblings survive (the parse
128
- // is per-layer lenient, not all-or-nothing).
129
- @Test
130
- fun skipsLayerWithoutShader() {
131
- CompositeLayers.set("""[{"target":"background"},{"shader":"direct","target":"subject"}]""")
132
- val layers = CompositeLayers.get()
133
- assertEquals(1, layers.size)
134
- assertEquals("direct", layers[0].shader)
135
- }
136
-
137
- @Test
138
- fun parsesEmptyStack() {
139
- CompositeLayers.set("[]")
140
- assertTrue(CompositeLayers.get().isEmpty())
141
- }
142
-
143
- // A whole-payload parse failure (not a JSON array) leaves the previous composite
144
- // in place rather than blanking the frame.
145
- @Test
146
- fun keepsPreviousCompositeOnMalformedPayload() {
147
- CompositeLayers.set("""[{"shader":"direct","target":"subject"}]""")
148
- CompositeLayers.set("not json at all")
149
- val layers = CompositeLayers.get()
150
- assertEquals(1, layers.size)
151
- assertEquals("direct", layers[0].shader)
152
- }
153
-
154
- // clear() drops the active composite (a non-composite effect taking over).
155
- @Test
156
- fun clearEmptiesStack() {
157
- CompositeLayers.set("""[{"shader":"direct","target":"subject"}]""")
158
- CompositeLayers.clear()
159
- assertTrue(CompositeLayers.get().isEmpty())
160
- }
161
-
162
- private companion object {
163
- const val EPS = 1e-6f
164
- }
165
- }
@@ -1,104 +0,0 @@
1
- // Loads a preset book as DATA by executing it under Bun with three shims
2
- // (issue #65). The book is real TypeScript that imports React control
3
- // components, expo-asset, and bundled images; none of those matter to a
4
- // thumbnail, so each is replaced with the smallest stand-in that keeps the
5
- // module graph executable:
6
- //
7
- // - `expo-asset`: `Asset.fromModule(m).uri` returns the module value itself,
8
- // which (via the asset shim below) is the file's absolute path.
9
- // - control components (`*.form.*`, `*.controls.*`): a Proxy that satisfies
10
- // any named import without pulling React in; presets reference these as
11
- // values but thumbnails never render them.
12
- // - bundled images (`.webp` / `.png` / `.jpg`): the file's absolute path as
13
- // the default export, so an image layer's `source` resolves to something
14
- // the CLI can read and embed.
15
- //
16
- // Executing (vs static parsing, which the prebuild plugin does for asset
17
- // collection) is what yields exact per-preset layer stacks and uniform values
18
- // with no fragile object-literal parsing. This is Bun-only by design; the
19
- // thumbnail maker is an opt-in dev command, not runtime code.
20
-
21
- import path from 'node:path';
22
- import { plugin } from 'bun';
23
-
24
- /** One layer as authored in a book (the subset thumbnails care about). */
25
- type LoadedLayer = {
26
- readonly id?: string;
27
- readonly shader: string;
28
- readonly target?: string;
29
- readonly blend?: string;
30
- readonly source?: string;
31
- readonly uniforms?: Record<string, number | readonly number[]>;
32
- };
33
-
34
- /** One preset as authored in a book. */
35
- export type LoadedPreset = {
36
- readonly name: string;
37
- readonly taxonomy: readonly string[];
38
- readonly thumbnail?: string | number;
39
- readonly layers: readonly LoadedLayer[];
40
- };
41
-
42
- plugin({
43
- name: 'kaleidoscope-thumbnails-book-shims',
44
- setup(build) {
45
- // Path-based onLoad (not onResolve, which Bun's runtime plugins do not
46
- // reliably fire): any file inside the expo-asset package becomes the
47
- // shim, so its entry never executes and never drags React Native's
48
- // Flow-typed asset registry in.
49
- build.onLoad({ filter: /node_modules\/expo-asset\/.*\.[cm]?js$/ }, () => ({
50
- contents: [
51
- 'function unwrap(m) {',
52
- ' if (typeof m === "string") return m;',
53
- ' try { if (m && typeof m.default === "string") return m.default; } catch {}',
54
- ' try { if (m && typeof m.uri === "string") return m.uri; } catch {}',
55
- ' try { return String(m); } catch { return ""; }',
56
- '}',
57
- 'export const Asset = { fromModule: (m) => ({ uri: unwrap(m) }) };',
58
- ].join('\n'),
59
- loader: 'js',
60
- }));
61
- // Control components are imported by NAME, and ESM named imports need the
62
- // name to exist; the name is convention-derived from the filename
63
- // (`clouds.controls.js` -> CloudsControls, `plasma.form.tsx` -> PlasmaForm),
64
- // so the stub synthesizes exactly that export.
65
- build.onLoad({ filter: /\.(form|controls)\.(tsx|jsx|ts|js)$/ }, (args) => {
66
- const base = path.basename(args.path);
67
- const stem = base.split('.')[0] ?? 'stub';
68
- const kind = base.includes('.controls.') ? 'Controls' : 'Form';
69
- const pascal = stem
70
- .split('-')
71
- .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
72
- .join('');
73
- const name = `${pascal}${kind}`;
74
- return {
75
- contents: `export const ${name} = () => null;\nexport default ${name};`,
76
- loader: 'js',
77
- };
78
- });
79
- // Bun materializes plugin modules as ESM namespaces even when authored as
80
- // CJS, so the path rides the default export; `import x from './x.webp'`
81
- // unwraps it natively and the Asset shim's unwrap() handles the
82
- // `require('./x.webp')` namespace form.
83
- build.onLoad({ filter: /\.(webp|png|jpe?g|gif)$/ }, (args) => ({
84
- contents: `export default ${JSON.stringify(args.path)};`,
85
- loader: 'js',
86
- }));
87
- },
88
- });
89
-
90
- /**
91
- * Import the book and return its preset map. Accepts the conventional
92
- * `export const presets` (the demo's shape) or a default export.
93
- */
94
- export async function loadPresetBook(bookPath: string): Promise<Record<string, LoadedPreset>> {
95
- const abs = path.resolve(bookPath);
96
- const mod = (await import(abs)) as { presets?: unknown; default?: unknown };
97
- const book = mod.presets ?? mod.default;
98
- if (!book || typeof book !== 'object') {
99
- throw new Error(
100
- `${bookPath} did not export a preset book (expected \`export const presets = {...}\` or a default export).`,
101
- );
102
- }
103
- return book as Record<string, LoadedPreset>;
104
- }