react-native-theme-reveal 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +734 -2
  3. package/android/build.gradle +32 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/baseline-prof.txt +4 -0
  6. package/android/src/main/java/expo/modules/themereveal/CapturePlan.kt +199 -0
  7. package/android/src/main/java/expo/modules/themereveal/CaptureRules.kt +50 -0
  8. package/android/src/main/java/expo/modules/themereveal/GpuSnapshot.kt +367 -0
  9. package/android/src/main/java/expo/modules/themereveal/KeptBitmaps.kt +102 -0
  10. package/android/src/main/java/expo/modules/themereveal/KeptTarget.kt +103 -0
  11. package/android/src/main/java/expo/modules/themereveal/Motion.kt +79 -0
  12. package/android/src/main/java/expo/modules/themereveal/Once.kt +13 -0
  13. package/android/src/main/java/expo/modules/themereveal/RenderThreadReveal.kt +107 -0
  14. package/android/src/main/java/expo/modules/themereveal/RevealOverlayView.kt +192 -0
  15. package/android/src/main/java/expo/modules/themereveal/SecondSnapshotWait.kt +112 -0
  16. package/android/src/main/java/expo/modules/themereveal/ShapePath.kt +47 -0
  17. package/android/src/main/java/expo/modules/themereveal/ThemeRevealModule.kt +904 -0
  18. package/android/src/main/java/expo/modules/themereveal/TransitionArgs.kt +50 -0
  19. package/android/src/main/java/expo/modules/themereveal/WindowSnapshot.kt +536 -0
  20. package/build/ThemeRevealModule.d.ts +11 -0
  21. package/build/ThemeRevealModule.d.ts.map +1 -0
  22. package/build/ThemeRevealModule.js +11 -0
  23. package/build/ThemeRevealModule.js.map +1 -0
  24. package/build/createThemeReveal.d.ts +39 -0
  25. package/build/createThemeReveal.d.ts.map +1 -0
  26. package/build/createThemeReveal.js +186 -0
  27. package/build/createThemeReveal.js.map +1 -0
  28. package/build/index.d.ts +9 -0
  29. package/build/index.d.ts.map +1 -0
  30. package/build/index.js +5 -0
  31. package/build/index.js.map +1 -0
  32. package/build/originFromPress.d.ts +12 -0
  33. package/build/originFromPress.d.ts.map +1 -0
  34. package/build/originFromPress.js +26 -0
  35. package/build/originFromPress.js.map +1 -0
  36. package/build/resolveTransition.d.ts +13 -0
  37. package/build/resolveTransition.d.ts.map +1 -0
  38. package/build/resolveTransition.js +43 -0
  39. package/build/resolveTransition.js.map +1 -0
  40. package/build/store.d.ts +8 -0
  41. package/build/store.d.ts.map +1 -0
  42. package/build/store.js +23 -0
  43. package/build/store.js.map +1 -0
  44. package/build/types.d.ts +31 -0
  45. package/build/types.d.ts.map +1 -0
  46. package/build/types.js +2 -0
  47. package/build/types.js.map +1 -0
  48. package/expo-module.config.json +9 -0
  49. package/ios/Motion.swift +171 -0
  50. package/ios/RevealOverlay.swift +115 -0
  51. package/ios/SVGPath.swift +373 -0
  52. package/ios/ThemeReveal.podspec +26 -0
  53. package/ios/ThemeRevealModule.swift +264 -0
  54. package/mocks/ThemeReveal.js +10 -0
  55. package/package.json +67 -4
  56. package/src/ThemeRevealModule.ts +24 -0
  57. package/src/createThemeReveal.ts +262 -0
  58. package/src/index.ts +14 -0
  59. package/src/originFromPress.ts +40 -0
  60. package/src/resolveTransition.ts +63 -0
  61. package/src/store.ts +30 -0
  62. package/src/types.ts +17 -0
@@ -0,0 +1,186 @@
1
+ import { useEffect, useLayoutEffect } from 'react';
2
+ import ThemeRevealModule from './ThemeRevealModule';
3
+ import { pagePointOf } from './originFromPress';
4
+ import { resolveTransition } from './resolveTransition';
5
+ import { createStore, useStore } from './store';
6
+ const timestampOf = (event) => {
7
+ const { timestamp } = event?.nativeEvent ?? {};
8
+ return typeof timestamp === 'number' && Number.isFinite(timestamp) ? timestamp : undefined;
9
+ };
10
+ // A capture that no switch uses is freed after this long. Native frees it after the same time on its own.
11
+ const CAPTURE_TIMEOUT_MS = 3000;
12
+ export function createThemeReveal(options) {
13
+ const { getTheme, setTheme } = options;
14
+ const warn = (error) => console.warn(error);
15
+ // A throwing user callback must not skip setTheme or leave an unhandled rejection.
16
+ const call = (callback, arg) => {
17
+ try {
18
+ callback?.(arg);
19
+ }
20
+ catch (thrown) {
21
+ warn(thrown);
22
+ }
23
+ };
24
+ const onError = (error) => call(options.onError ?? warn, error);
25
+ const onSettled = (info) => call(options.onSettled, info);
26
+ const store = createStore({ pending: null, held: null });
27
+ let pendingOrigin = null;
28
+ let captured = null;
29
+ let latestId = 0;
30
+ let warmedUp = false;
31
+ const setOrigin = (point, viewTag) => {
32
+ pendingOrigin = { point, viewTag };
33
+ };
34
+ // Forgets the JS side of the capture and returns it. Native keeps it until prepare, cancel or dropCapture.
35
+ const releaseCapture = () => {
36
+ const current = captured;
37
+ captured = null;
38
+ if (current) {
39
+ clearTimeout(current.expiry);
40
+ clearTimeout(current.drop);
41
+ }
42
+ return current;
43
+ };
44
+ const freeCapture = () => ThemeRevealModule.dropCapture().catch(onError);
45
+ const startCapture = (point, viewTag, pressedAt) => {
46
+ releaseCapture();
47
+ const origin = { point, viewTag };
48
+ pendingOrigin = origin;
49
+ // A long press keeps its point for the switch on release, but not the snapshot.
50
+ const current = {
51
+ origin,
52
+ pressedAt,
53
+ expiry: setTimeout(() => {
54
+ if (captured !== current)
55
+ return;
56
+ current.expired = true;
57
+ freeCapture();
58
+ }, CAPTURE_TIMEOUT_MS),
59
+ };
60
+ captured = current;
61
+ ThemeRevealModule.capture(viewTag).catch(onError);
62
+ };
63
+ const capture = (point, viewTag) => startCapture(point, viewTag, undefined);
64
+ const dropCapture = () => {
65
+ const current = releaseCapture();
66
+ if (current && pendingOrigin === current.origin)
67
+ pendingOrigin = null;
68
+ if (!current?.expired)
69
+ freeCapture();
70
+ };
71
+ // On release React Native calls onPressOut before onPress in the same dispatch (or up to 130 ms after onPress
72
+ // for a short press), and a cancelled press gets onPressOut with no onPress. So onPressOut only drops the
73
+ // capture in a later macrotask, and only if no switchTo took it by then. A late onPressOut whose touch is older
74
+ // than the capture's press-in belongs to an earlier press and leaves the capture alone.
75
+ const pressCapture = {
76
+ onPressIn: (event) => {
77
+ const origin = pagePointOf(event);
78
+ if (origin)
79
+ startCapture(origin.point, origin.viewTag, timestampOf(event));
80
+ },
81
+ onPressOut: (event) => {
82
+ const current = captured;
83
+ if (!current)
84
+ return;
85
+ const releasedAt = timestampOf(event);
86
+ if (current.pressedAt !== undefined && releasedAt !== undefined && releasedAt < current.pressedAt)
87
+ return;
88
+ clearTimeout(current.drop);
89
+ current.drop = setTimeout(() => {
90
+ if (captured === current)
91
+ dropCapture();
92
+ }, 0);
93
+ },
94
+ };
95
+ const switchTo = async (theme, switchOptions) => {
96
+ const held = releaseCapture();
97
+ const origin = pendingOrigin;
98
+ pendingOrigin = null;
99
+ const id = ++latestId;
100
+ const startedAt = performance.now();
101
+ if (Object.is(getTheme(), theme)) {
102
+ if (held && !held.expired)
103
+ freeCapture();
104
+ try {
105
+ await ThemeRevealModule.cancel();
106
+ }
107
+ catch (error) {
108
+ onError(error);
109
+ }
110
+ return;
111
+ }
112
+ let capture = null;
113
+ try {
114
+ capture = await ThemeRevealModule.prepare(origin?.viewTag);
115
+ }
116
+ catch (error) {
117
+ onError(error);
118
+ }
119
+ if (id !== latestId)
120
+ return;
121
+ const prepareMs = performance.now() - startedAt;
122
+ if (!capture) {
123
+ setTheme(theme);
124
+ onSettled({ transition: null, capture: null, prepareMs, totalMs: performance.now() - startedAt });
125
+ return;
126
+ }
127
+ const transition = resolveTransition({
128
+ requested: switchOptions?.transition,
129
+ fallback: options.transition,
130
+ origin: origin?.point ?? null,
131
+ shape: options.shape,
132
+ durations: options.durations,
133
+ wipeDirection: options.wipeDirection,
134
+ requestedShape: switchOptions?.shape,
135
+ requestedWipeDirection: switchOptions?.wipeDirection,
136
+ });
137
+ store.set({ held: { id, theme: getTheme() } });
138
+ try {
139
+ setTheme(theme);
140
+ }
141
+ catch (error) {
142
+ if (store.get().held?.id === id)
143
+ store.set({ held: null });
144
+ ThemeRevealModule.cancel().catch(onError);
145
+ throw error;
146
+ }
147
+ store.set({ pending: { id, transition, capture, startedAt, prepareMs } });
148
+ };
149
+ // Once per instance: start the native capture path early so the first switch doesn't pay for it.
150
+ const warmUp = () => {
151
+ if (warmedUp || options.warmUp === false)
152
+ return;
153
+ warmedUp = true;
154
+ try {
155
+ ThemeRevealModule.warmUp().catch(onError);
156
+ }
157
+ catch (error) {
158
+ onError(error);
159
+ }
160
+ };
161
+ const useStart = () => {
162
+ const pending = useStore(store, (state) => state.pending);
163
+ useEffect(warmUp, []);
164
+ useLayoutEffect(() => {
165
+ if (!pending || store.get().pending !== pending)
166
+ return;
167
+ store.set({ pending: null });
168
+ const { id, transition, capture, startedAt, prepareMs } = pending;
169
+ ThemeRevealModule.start(transition)
170
+ .catch(onError)
171
+ .finally(() => {
172
+ if (store.get().held?.id === id)
173
+ store.set({ held: null });
174
+ onSettled({
175
+ transition: transition.type,
176
+ capture,
177
+ prepareMs,
178
+ totalMs: performance.now() - startedAt,
179
+ });
180
+ });
181
+ }, [pending]);
182
+ };
183
+ const useHeldTheme = () => useStore(store, (state) => state.held?.theme ?? null);
184
+ return { setOrigin, capture, dropCapture, pressCapture, switchTo, useStart, useHeldTheme };
185
+ }
186
+ //# sourceMappingURL=createThemeReveal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createThemeReveal.js","sourceRoot":"","sources":["../src/createThemeReveal.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useLayoutEffect } from 'react'\nimport type { GestureResponderEvent } from 'react-native'\n\nimport ThemeRevealModule from './ThemeRevealModule'\nimport { pagePointOf } from './originFromPress'\nimport { resolveTransition } from './resolveTransition'\nimport { createStore, useStore } from './store'\nimport type { CapturePath, Point, Shape, Transition, TransitionType, WipeDirection } from './types'\n\nexport type SettledInfo = {\n transition: TransitionType | null\n // How the snapshot was taken, or null when there was none.\n capture: CapturePath | null\n prepareMs: number\n totalMs: number\n}\n\nexport type ThemeRevealOptions<T> = {\n getTheme: () => T\n setTheme: (theme: T) => void\n shape?: Shape\n durations?: Partial<Record<TransitionType, number>>\n transition?: TransitionType\n wipeDirection?: WipeDirection\n onError?: (error: unknown) => void\n onSettled?: (info: SettledInfo) => void\n // Prepare the native snapshot path when useStart mounts (Android: the copy thread, then the GPU snapshot on API 29\n // and up, or one kept window-size bitmap below). Default true.\n warmUp?: boolean\n}\n\nexport type SwitchOptions = {\n transition?: TransitionType\n shape?: Shape\n wipeDirection?: WipeDirection\n}\n\n// Spread on a Pressable whose onPress calls switchTo: the snapshot is taken on press-in, and a press that ends\n// with no switch frees it.\nexport type PressCapture = {\n onPressIn(event: GestureResponderEvent): void\n onPressOut(event?: GestureResponderEvent): void\n}\n\nexport type ThemeReveal<T> = {\n setOrigin(point: Point, viewTag?: number): void\n capture(point: Point, viewTag?: number): void\n dropCapture(): void\n pressCapture: PressCapture\n switchTo(theme: T, options?: SwitchOptions): Promise<void>\n useStart(): void\n useHeldTheme(): T | null\n}\n\ntype PendingStart = {\n id: number\n transition: Transition\n capture: CapturePath\n startedAt: number\n prepareMs: number\n}\n\ntype Origin = { point: Point; viewTag?: number }\n\ntype Captured = {\n origin: Origin\n expiry: ReturnType<typeof setTimeout>\n drop?: ReturnType<typeof setTimeout>\n // Touch time of the press-in, to tell this press's onPressOut from a late one of an earlier press.\n pressedAt?: number\n // Native freed the snapshot after the timeout. The origin stays until the press ends.\n expired?: boolean\n}\n\nconst timestampOf = (event: GestureResponderEvent | undefined): number | undefined => {\n const { timestamp }: { timestamp?: unknown } = event?.nativeEvent ?? {}\n return typeof timestamp === 'number' && Number.isFinite(timestamp) ? timestamp : undefined\n}\n\n// A capture that no switch uses is freed after this long. Native frees it after the same time on its own.\nconst CAPTURE_TIMEOUT_MS = 3000\n\ntype RevealState<T> = {\n pending: PendingStart | null\n held: { id: number; theme: T } | null\n}\n\nexport function createThemeReveal<T>(options: ThemeRevealOptions<T>): ThemeReveal<T> {\n const { getTheme, setTheme } = options\n const warn = (error: unknown) => console.warn(error)\n // A throwing user callback must not skip setTheme or leave an unhandled rejection.\n const call = <A>(callback: ((arg: A) => void) | undefined, arg: A) => {\n try {\n callback?.(arg)\n } catch (thrown) {\n warn(thrown)\n }\n }\n const onError = (error: unknown) => call(options.onError ?? warn, error)\n const onSettled = (info: SettledInfo) => call(options.onSettled, info)\n const store = createStore<RevealState<T>>({ pending: null, held: null })\n let pendingOrigin: Origin | null = null\n let captured: Captured | null = null\n let latestId = 0\n let warmedUp = false\n\n const setOrigin = (point: Point, viewTag?: number) => {\n pendingOrigin = { point, viewTag }\n }\n\n // Forgets the JS side of the capture and returns it. Native keeps it until prepare, cancel or dropCapture.\n const releaseCapture = () => {\n const current = captured\n captured = null\n if (current) {\n clearTimeout(current.expiry)\n clearTimeout(current.drop)\n }\n return current\n }\n\n const freeCapture = () => ThemeRevealModule.dropCapture().catch(onError)\n\n const startCapture = (point: Point, viewTag: number | undefined, pressedAt: number | undefined) => {\n releaseCapture()\n const origin = { point, viewTag }\n pendingOrigin = origin\n // A long press keeps its point for the switch on release, but not the snapshot.\n const current: Captured = {\n origin,\n pressedAt,\n expiry: setTimeout(() => {\n if (captured !== current) return\n current.expired = true\n freeCapture()\n }, CAPTURE_TIMEOUT_MS),\n }\n captured = current\n ThemeRevealModule.capture(viewTag).catch(onError)\n }\n\n const capture = (point: Point, viewTag?: number) => startCapture(point, viewTag, undefined)\n\n const dropCapture = () => {\n const current = releaseCapture()\n if (current && pendingOrigin === current.origin) pendingOrigin = null\n if (!current?.expired) freeCapture()\n }\n\n // On release React Native calls onPressOut before onPress in the same dispatch (or up to 130 ms after onPress\n // for a short press), and a cancelled press gets onPressOut with no onPress. So onPressOut only drops the\n // capture in a later macrotask, and only if no switchTo took it by then. A late onPressOut whose touch is older\n // than the capture's press-in belongs to an earlier press and leaves the capture alone.\n const pressCapture: PressCapture = {\n onPressIn: (event) => {\n const origin = pagePointOf(event)\n if (origin) startCapture(origin.point, origin.viewTag, timestampOf(event))\n },\n onPressOut: (event) => {\n const current = captured\n if (!current) return\n const releasedAt = timestampOf(event)\n if (current.pressedAt !== undefined && releasedAt !== undefined && releasedAt < current.pressedAt)\n return\n clearTimeout(current.drop)\n current.drop = setTimeout(() => {\n if (captured === current) dropCapture()\n }, 0)\n },\n }\n\n const switchTo = async (theme: T, switchOptions?: SwitchOptions) => {\n const held = releaseCapture()\n const origin = pendingOrigin\n pendingOrigin = null\n const id = ++latestId\n const startedAt = performance.now()\n\n if (Object.is(getTheme(), theme)) {\n if (held && !held.expired) freeCapture()\n try {\n await ThemeRevealModule.cancel()\n } catch (error) {\n onError(error)\n }\n return\n }\n\n let capture: CapturePath | null = null\n try {\n capture = await ThemeRevealModule.prepare(origin?.viewTag)\n } catch (error) {\n onError(error)\n }\n if (id !== latestId) return\n const prepareMs = performance.now() - startedAt\n\n if (!capture) {\n setTheme(theme)\n onSettled({ transition: null, capture: null, prepareMs, totalMs: performance.now() - startedAt })\n return\n }\n\n const transition = resolveTransition({\n requested: switchOptions?.transition,\n fallback: options.transition,\n origin: origin?.point ?? null,\n shape: options.shape,\n durations: options.durations,\n wipeDirection: options.wipeDirection,\n requestedShape: switchOptions?.shape,\n requestedWipeDirection: switchOptions?.wipeDirection,\n })\n store.set({ held: { id, theme: getTheme() } })\n try {\n setTheme(theme)\n } catch (error) {\n if (store.get().held?.id === id) store.set({ held: null })\n ThemeRevealModule.cancel().catch(onError)\n throw error\n }\n store.set({ pending: { id, transition, capture, startedAt, prepareMs } })\n }\n\n // Once per instance: start the native capture path early so the first switch doesn't pay for it.\n const warmUp = () => {\n if (warmedUp || options.warmUp === false) return\n warmedUp = true\n try {\n ThemeRevealModule.warmUp().catch(onError)\n } catch (error) {\n onError(error)\n }\n }\n\n const useStart = () => {\n const pending = useStore(store, (state) => state.pending)\n\n useEffect(warmUp, [])\n\n useLayoutEffect(() => {\n if (!pending || store.get().pending !== pending) return\n store.set({ pending: null })\n const { id, transition, capture, startedAt, prepareMs } = pending\n ThemeRevealModule.start(transition)\n .catch(onError)\n .finally(() => {\n if (store.get().held?.id === id) store.set({ held: null })\n onSettled({\n transition: transition.type,\n capture,\n prepareMs,\n totalMs: performance.now() - startedAt,\n })\n })\n }, [pending])\n }\n\n const useHeldTheme = () => useStore(store, (state) => state.held?.theme ?? null)\n\n return { setOrigin, capture, dropCapture, pressCapture, switchTo, useStart, useHeldTheme }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export { default as ThemeRevealModule } from './ThemeRevealModule';
2
+ export { createThemeReveal } from './createThemeReveal';
3
+ export { originFromPress } from './originFromPress';
4
+ export type { MeasurableView, PressOrigin } from './originFromPress';
5
+ export { resolveTransition } from './resolveTransition';
6
+ export type { ResolveTransitionInput } from './resolveTransition';
7
+ export type { PressCapture, SettledInfo, SwitchOptions, ThemeReveal, ThemeRevealOptions, } from './createThemeReveal';
8
+ export type { CapturePath, Point, Shape, Transition, TransitionType, WipeDirection } from './types';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,YAAY,EACV,YAAY,EACZ,WAAW,EACX,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { default as ThemeRevealModule } from './ThemeRevealModule';
2
+ export { createThemeReveal } from './createThemeReveal';
3
+ export { originFromPress } from './originFromPress';
4
+ export { resolveTransition } from './resolveTransition';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA","sourcesContent":["export { default as ThemeRevealModule } from './ThemeRevealModule'\nexport { createThemeReveal } from './createThemeReveal'\nexport { originFromPress } from './originFromPress'\nexport type { MeasurableView, PressOrigin } from './originFromPress'\nexport { resolveTransition } from './resolveTransition'\nexport type { ResolveTransitionInput } from './resolveTransition'\nexport type {\n PressCapture,\n SettledInfo,\n SwitchOptions,\n ThemeReveal,\n ThemeRevealOptions,\n} from './createThemeReveal'\nexport type { CapturePath, Point, Shape, Transition, TransitionType, WipeDirection } from './types'\n"]}
@@ -0,0 +1,12 @@
1
+ import type { GestureResponderEvent } from 'react-native';
2
+ import type { Point } from './types';
3
+ export type MeasurableView = {
4
+ measureInWindow(callback: (x: number, y: number, width: number, height: number) => void): void;
5
+ };
6
+ export type PressOrigin = {
7
+ point: Point;
8
+ viewTag?: number;
9
+ };
10
+ export declare function pagePointOf(event: GestureResponderEvent): PressOrigin | null;
11
+ export declare function originFromPress(event: GestureResponderEvent, fallbackView?: MeasurableView | null): Promise<PressOrigin | null>;
12
+ //# sourceMappingURL=originFromPress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"originFromPress.d.ts","sourceRoot":"","sources":["../src/originFromPress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,MAAM,MAAM,cAAc,GAAG;IAC3B,eAAe,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAA;CAC/F,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAW5D,wBAAgB,WAAW,CAAC,KAAK,EAAE,qBAAqB,GAAG,WAAW,GAAG,IAAI,CAI5E;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,qBAAqB,EAC5B,YAAY,CAAC,EAAE,cAAc,GAAG,IAAI,GACnC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAW7B"}
@@ -0,0 +1,26 @@
1
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
2
+ const viewTagOf = (event) => {
3
+ const { target } = event.nativeEvent;
4
+ return typeof target === 'number' ? { viewTag: target } : {};
5
+ };
6
+ // Touch point and target tag of a press event, or null when the event has no page point.
7
+ export function pagePointOf(event) {
8
+ const { pageX, pageY } = event.nativeEvent;
9
+ if (!isFiniteNumber(pageX) || !isFiniteNumber(pageY))
10
+ return null;
11
+ return { point: { x: pageX, y: pageY }, ...viewTagOf(event) };
12
+ }
13
+ export function originFromPress(event, fallbackView) {
14
+ const tag = viewTagOf(event);
15
+ const origin = pagePointOf(event);
16
+ if (origin)
17
+ return Promise.resolve(origin);
18
+ if (!fallbackView)
19
+ return Promise.resolve(null);
20
+ return new Promise((resolve) => {
21
+ fallbackView.measureInWindow((x, y, width, height) => {
22
+ resolve({ point: { x: x + width / 2, y: y + height / 2 }, ...tag });
23
+ });
24
+ });
25
+ }
26
+ //# sourceMappingURL=originFromPress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"originFromPress.js","sourceRoot":"","sources":["../src/originFromPress.ts"],"names":[],"mappings":"AAUA,MAAM,cAAc,GAAG,CAAC,KAAc,EAAmB,EAAE,CACzD,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAErD,MAAM,SAAS,GAAG,CAAC,KAA4B,EAAwB,EAAE;IACvE,MAAM,EAAE,MAAM,EAAE,GAAyB,KAAK,CAAC,WAAW,CAAA;IAC1D,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AAC9D,CAAC,CAAA;AAED,yFAAyF;AACzF,MAAM,UAAU,WAAW,CAAC,KAA4B;IACtD,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAyC,KAAK,CAAC,WAAW,CAAA;IAChF,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACjE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,EAAE,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAA4B,EAC5B,YAAoC;IAEpC,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;IAC5B,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IAEjC,IAAI,MAAM;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,YAAY,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YACnD,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,EAAE,GAAG,GAAG,EAAE,CAAC,CAAA;QACrE,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type { GestureResponderEvent } from 'react-native'\n\nimport type { Point } from './types'\n\nexport type MeasurableView = {\n measureInWindow(callback: (x: number, y: number, width: number, height: number) => void): void\n}\n\nexport type PressOrigin = { point: Point; viewTag?: number }\n\nconst isFiniteNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\nconst viewTagOf = (event: GestureResponderEvent): { viewTag?: number } => {\n const { target }: { target?: unknown } = event.nativeEvent\n return typeof target === 'number' ? { viewTag: target } : {}\n}\n\n// Touch point and target tag of a press event, or null when the event has no page point.\nexport function pagePointOf(event: GestureResponderEvent): PressOrigin | null {\n const { pageX, pageY }: { pageX?: unknown; pageY?: unknown } = event.nativeEvent\n if (!isFiniteNumber(pageX) || !isFiniteNumber(pageY)) return null\n return { point: { x: pageX, y: pageY }, ...viewTagOf(event) }\n}\n\nexport function originFromPress(\n event: GestureResponderEvent,\n fallbackView?: MeasurableView | null,\n): Promise<PressOrigin | null> {\n const tag = viewTagOf(event)\n const origin = pagePointOf(event)\n\n if (origin) return Promise.resolve(origin)\n if (!fallbackView) return Promise.resolve(null)\n return new Promise((resolve) => {\n fallbackView.measureInWindow((x, y, width, height) => {\n resolve({ point: { x: x + width / 2, y: y + height / 2 }, ...tag })\n })\n })\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import type { Point, Shape, Transition, TransitionType, WipeDirection } from './types';
2
+ export type ResolveTransitionInput = {
3
+ requested?: TransitionType;
4
+ fallback?: TransitionType;
5
+ origin: Point | null;
6
+ shape?: Shape;
7
+ durations?: Partial<Record<TransitionType, number>>;
8
+ wipeDirection?: WipeDirection;
9
+ requestedShape?: Shape;
10
+ requestedWipeDirection?: WipeDirection;
11
+ };
12
+ export declare function resolveTransition(input: ResolveTransitionInput): Transition;
13
+ //# sourceMappingURL=resolveTransition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveTransition.d.ts","sourceRoot":"","sources":["../src/resolveTransition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,CAAC,EAAE,cAAc,CAAA;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,MAAM,EAAE,KAAK,GAAG,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,cAAc,CAAC,EAAE,KAAK,CAAA;IACtB,sBAAsB,CAAC,EAAE,aAAa,CAAA;CACvC,CAAA;AA0BD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,UAAU,CAyB3E"}
@@ -0,0 +1,43 @@
1
+ const DEFAULT_DURATIONS = {
2
+ reveal: 450,
3
+ inverseReveal: 450,
4
+ wipe: 400,
5
+ crossfade: 300,
6
+ };
7
+ const MAX_DURATION_MS = 10_000;
8
+ const isValidDuration = (ms) => ms !== undefined && Number.isFinite(ms) && ms > 0 && ms <= MAX_DURATION_MS;
9
+ const isNumber = (value) => typeof value === 'number' && Number.isFinite(value);
10
+ const isPoint = (value) => typeof value === 'object' && value !== null && isNumber(value.x) && isNumber(value.y);
11
+ const isShape = (value) => typeof value === 'object' &&
12
+ value !== null &&
13
+ typeof value.path === 'string' &&
14
+ isPoint(value.anchor) &&
15
+ isNumber(value.inscribedRadius);
16
+ export function resolveTransition(input) {
17
+ const { requested, fallback, durations } = input;
18
+ // Untyped callers can pass null, undefined or other non-numbers inside origin or shape. Android rejects start for
19
+ // those before its parser runs, so they fall back here: no origin, and the circle.
20
+ const origin = isPoint(input.origin) ? input.origin : null;
21
+ const type = requested ?? fallback ?? (origin ? 'reveal' : 'crossfade');
22
+ const requestedShape = input.requestedShape ?? input.shape;
23
+ const shape = isShape(requestedShape) ? requestedShape : undefined;
24
+ const wipeDirection = input.requestedWipeDirection ?? input.wipeDirection;
25
+ const durationOf = (kind) => {
26
+ const ms = durations?.[kind];
27
+ return isValidDuration(ms) ? ms : DEFAULT_DURATIONS[kind];
28
+ };
29
+ switch (type) {
30
+ case 'reveal':
31
+ case 'inverseReveal':
32
+ if (!origin)
33
+ return { type: 'crossfade', durationMs: durationOf('crossfade') };
34
+ return shape
35
+ ? { type, origin, durationMs: durationOf(type), shape }
36
+ : { type, origin, durationMs: durationOf(type) };
37
+ case 'wipe':
38
+ return { type, direction: wipeDirection ?? 'up', durationMs: durationOf(type) };
39
+ case 'crossfade':
40
+ return { type, durationMs: durationOf(type) };
41
+ }
42
+ }
43
+ //# sourceMappingURL=resolveTransition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveTransition.js","sourceRoot":"","sources":["../src/resolveTransition.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Point, Shape, Transition, TransitionType, WipeDirection } from './types'\n\nexport type ResolveTransitionInput = {\n requested?: TransitionType\n fallback?: TransitionType\n origin: Point | null\n shape?: Shape\n durations?: Partial<Record<TransitionType, number>>\n wipeDirection?: WipeDirection\n requestedShape?: Shape\n requestedWipeDirection?: WipeDirection\n}\n\nconst DEFAULT_DURATIONS: Record<TransitionType, number> = {\n reveal: 450,\n inverseReveal: 450,\n wipe: 400,\n crossfade: 300,\n}\n\nconst MAX_DURATION_MS = 10_000\n\nconst isValidDuration = (ms: number | undefined): ms is number =>\n ms !== undefined && Number.isFinite(ms) && ms > 0 && ms <= MAX_DURATION_MS\n\nconst isNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)\n\nconst isPoint = (value: unknown): value is Point =>\n typeof value === 'object' && value !== null && isNumber((value as Point).x) && isNumber((value as Point).y)\n\nconst isShape = (value: unknown): value is Shape =>\n typeof value === 'object' &&\n value !== null &&\n typeof (value as Shape).path === 'string' &&\n isPoint((value as Shape).anchor) &&\n isNumber((value as Shape).inscribedRadius)\n\nexport function resolveTransition(input: ResolveTransitionInput): Transition {\n const { requested, fallback, durations } = input\n // Untyped callers can pass null, undefined or other non-numbers inside origin or shape. Android rejects start for\n // those before its parser runs, so they fall back here: no origin, and the circle.\n const origin = isPoint(input.origin) ? input.origin : null\n const type = requested ?? fallback ?? (origin ? 'reveal' : 'crossfade')\n const requestedShape = input.requestedShape ?? input.shape\n const shape = isShape(requestedShape) ? requestedShape : undefined\n const wipeDirection = input.requestedWipeDirection ?? input.wipeDirection\n const durationOf = (kind: TransitionType) => {\n const ms = durations?.[kind]\n return isValidDuration(ms) ? ms : DEFAULT_DURATIONS[kind]\n }\n switch (type) {\n case 'reveal':\n case 'inverseReveal':\n if (!origin) return { type: 'crossfade', durationMs: durationOf('crossfade') }\n return shape\n ? { type, origin, durationMs: durationOf(type), shape }\n : { type, origin, durationMs: durationOf(type) }\n case 'wipe':\n return { type, direction: wipeDirection ?? 'up', durationMs: durationOf(type) }\n case 'crossfade':\n return { type, durationMs: durationOf(type) }\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ export type Store<S> = {
2
+ get(): S;
3
+ set(partial: Partial<S>): void;
4
+ subscribe(listener: () => void): () => void;
5
+ };
6
+ export declare function createStore<S>(initial: S): Store<S>;
7
+ export declare function useStore<S, V>(store: Store<S>, select: (state: S) => V): V;
8
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IACrB,GAAG,IAAI,CAAC,CAAA;IACR,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IAC9B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC5C,CAAA;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAgBnD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAG1E"}
package/build/store.js ADDED
@@ -0,0 +1,23 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ export function createStore(initial) {
3
+ let state = initial;
4
+ const listeners = new Set();
5
+ return {
6
+ get: () => state,
7
+ set: (partial) => {
8
+ state = { ...state, ...partial };
9
+ listeners.forEach((listener) => listener());
10
+ },
11
+ subscribe: (listener) => {
12
+ listeners.add(listener);
13
+ return () => {
14
+ listeners.delete(listener);
15
+ };
16
+ },
17
+ };
18
+ }
19
+ export function useStore(store, select) {
20
+ const getSnapshot = () => select(store.get());
21
+ return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
22
+ }
23
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAQ5C,MAAM,UAAU,WAAW,CAAI,OAAU;IACvC,IAAI,KAAK,GAAG,OAAO,CAAA;IACnB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;IACvC,OAAO;QACL,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK;QAChB,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,OAAO,EAAE,CAAA;YAChC,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC7C,CAAC;QACD,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;YACtB,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACvB,OAAO,GAAG,EAAE;gBACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;YAC5B,CAAC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAO,KAAe,EAAE,MAAuB;IACrE,MAAM,WAAW,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAA;IAC7C,OAAO,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,CAAC,CAAA;AACxE,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react'\n\nexport type Store<S> = {\n get(): S\n set(partial: Partial<S>): void\n subscribe(listener: () => void): () => void\n}\n\nexport function createStore<S>(initial: S): Store<S> {\n let state = initial\n const listeners = new Set<() => void>()\n return {\n get: () => state,\n set: (partial) => {\n state = { ...state, ...partial }\n listeners.forEach((listener) => listener())\n },\n subscribe: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n}\n\nexport function useStore<S, V>(store: Store<S>, select: (state: S) => V): V {\n const getSnapshot = () => select(store.get())\n return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)\n}\n"]}
@@ -0,0 +1,31 @@
1
+ export type Point = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ export type Shape = {
6
+ path: string;
7
+ anchor: Point;
8
+ inscribedRadius: number;
9
+ };
10
+ export type WipeDirection = 'up' | 'down' | 'left' | 'right';
11
+ export type Transition = {
12
+ type: 'reveal';
13
+ origin: Point;
14
+ durationMs: number;
15
+ shape?: Shape;
16
+ } | {
17
+ type: 'inverseReveal';
18
+ origin: Point;
19
+ durationMs: number;
20
+ shape?: Shape;
21
+ } | {
22
+ type: 'wipe';
23
+ direction: WipeDirection;
24
+ durationMs: number;
25
+ } | {
26
+ type: 'crossfade';
27
+ durationMs: number;
28
+ };
29
+ export type TransitionType = Transition['type'];
30
+ export type CapturePath = 'gpu' | 'pixelcopy' | 'view';
31
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5C,MAAM,MAAM,KAAK,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5E,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAE5D,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7C,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;AAI/C,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,MAAM,CAAA"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type Point = { x: number; y: number }\n\nexport type Shape = { path: string; anchor: Point; inscribedRadius: number }\n\nexport type WipeDirection = 'up' | 'down' | 'left' | 'right'\n\nexport type Transition =\n | { type: 'reveal'; origin: Point; durationMs: number; shape?: Shape }\n | { type: 'inverseReveal'; origin: Point; durationMs: number; shape?: Shape }\n | { type: 'wipe'; direction: WipeDirection; durationMs: number }\n | { type: 'crossfade'; durationMs: number }\n\nexport type TransitionType = Transition['type']\n\n// How the snapshot was taken. Android: 'gpu' (a GPU draw of the window, API 29+) or 'pixelcopy' (a readback).\n// iOS: 'view' (snapshotView).\nexport type CapturePath = 'gpu' | 'pixelcopy' | 'view'\n"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "ios": {
4
+ "modules": ["ThemeRevealModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.themereveal.ThemeRevealModule"]
8
+ }
9
+ }
@@ -0,0 +1,171 @@
1
+ import CoreGraphics
2
+ import Foundation
3
+
4
+ enum WipeDirection: String {
5
+ case up, down, left, right
6
+ }
7
+
8
+ // Reveal shape in path units. The anchor goes on the origin, and the radius is the distance
9
+ // from the anchor to the nearest edge (a bad value falls back to half the bounds diagonal).
10
+ struct RevealShape {
11
+ let path: CGPath
12
+ let bounds: CGRect
13
+ let anchor: CGPoint
14
+ let inscribedRadius: CGFloat
15
+
16
+ static let circle = RevealShape(
17
+ path: CGPath(ellipseIn: CGRect(x: -1, y: -1, width: 2, height: 2), transform: nil),
18
+ anchor: .zero,
19
+ inscribedRadius: 1)
20
+
21
+ // An unparsable or empty path, or a non-finite anchor, falls back to the unit circle.
22
+ static func resolve(path: String, anchor: CGPoint, inscribedRadius: CGFloat) -> RevealShape {
23
+ parse(path: path, anchor: anchor, inscribedRadius: inscribedRadius) ?? circle
24
+ }
25
+
26
+ // nil for an unparsable or empty path, or a non-finite anchor. The path keeps nonzero
27
+ // fill (as on Android) inside the even-odd reveal mask.
28
+ static func parse(path: String, anchor: CGPoint, inscribedRadius: CGFloat) -> RevealShape? {
29
+ guard anchor.x.isFinite, anchor.y.isFinite, let parsed = SVGPath.parse(path) else { return nil }
30
+ let shape = RevealShape(
31
+ path: parsed.normalized(using: .winding), anchor: anchor, inscribedRadius: inscribedRadius)
32
+ guard shape.bounds.width > 0, shape.bounds.height > 0 else { return nil }
33
+ return shape
34
+ }
35
+
36
+ private init(path: CGPath, anchor: CGPoint, inscribedRadius: CGFloat) {
37
+ self.path = path
38
+ bounds = path.boundingBoxOfPath
39
+ self.anchor = anchor
40
+ self.inscribedRadius = Motion.effectiveInscribedRadius(
41
+ bounds: bounds, anchor: anchor, inscribedRadius: inscribedRadius)
42
+ }
43
+ }
44
+
45
+ enum Motion {
46
+ static let startHeight: CGFloat = 24
47
+ static let samplesPerSecond = 120.0
48
+ // Keyframes stop at 10 s of samples; a longer duration stretches them.
49
+ static let maxSeconds = 10.0
50
+ static let maxSamples = 1200
51
+ static let maxDurationMs = 10_000.0
52
+ static let easeX1 = 0.65
53
+ static let easeY1 = 0.0
54
+ static let easeX2 = 0.35
55
+ static let easeY2 = 1.0
56
+
57
+ private static let growthCurve = 1.4
58
+ private static let easeSteps = 24
59
+
60
+ static func growth(g: Double, from: Double, to: Double) -> Double {
61
+ from * pow(to / from, pow(g, growthCurve))
62
+ }
63
+
64
+ static func shrink(g: Double, from: Double, to: Double) -> Double {
65
+ max(0, growth(g: 1 - g, from: from, to: to) - from * g)
66
+ }
67
+
68
+ static func coverScale(
69
+ originX: CGFloat, originY: CGFloat, width: CGFloat, height: CGFloat, inscribedRadius: CGFloat
70
+ ) -> CGFloat {
71
+ hypot(max(originX, width - originX), max(originY, height - originY)) / inscribedRadius
72
+ }
73
+
74
+ static func easeInOut(_ t: Double) -> Double {
75
+ if t <= 0 { return 0 }
76
+ if t >= 1 { return 1 }
77
+ var lo = 0.0
78
+ var hi = 1.0
79
+ for _ in 0..<easeSteps {
80
+ let mid = (lo + hi) / 2
81
+ if bezier(mid, easeX1, easeX2) < t { lo = mid } else { hi = mid }
82
+ }
83
+ return bezier((lo + hi) / 2, easeY1, easeY2)
84
+ }
85
+
86
+ // Non-finite, zero, negative or overlong durations are invalid: the overlay would sit on
87
+ // screen too long. Same cap as Android.
88
+ static func durationSeconds(ms: Double) -> Double? {
89
+ guard ms.isFinite, ms > 0, ms <= maxDurationMs else { return nil }
90
+ return ms / 1000
91
+ }
92
+
93
+ static func canAnimate(active: Bool, reduceMotion: Bool) -> Bool {
94
+ active && !reduceMotion
95
+ }
96
+
97
+ static func sameSize(_ snapshot: CGSize, _ window: CGSize) -> Bool {
98
+ snapshot.width == window.width && snapshot.height == window.height
99
+ }
100
+
101
+ // A press-in capture no switch uses is dropped after this long. Same as Android.
102
+ static let captureTimeoutSeconds = 3.0
103
+
104
+ // A held capture can stand in for prepare's own snapshot if it shows the same window with the
105
+ // same presented screen (no modal presented or dismissed since), that window still has its
106
+ // size, and it's no older than the timeout.
107
+ static func canUseCapture(
108
+ sameWindow: Bool, samePresentation: Bool, captured: CGSize, current: CGSize, ageSeconds: Double
109
+ ) -> Bool {
110
+ sameWindow && samePresentation && sameSize(captured, current) && ageSeconds >= 0
111
+ && ageSeconds < captureTimeoutSeconds
112
+ }
113
+
114
+ static func effectiveInscribedRadius(bounds: CGRect, anchor: CGPoint, inscribedRadius: CGFloat) -> CGFloat {
115
+ let anchorInside = anchor.x >= bounds.minX && anchor.x <= bounds.maxX
116
+ && anchor.y >= bounds.minY && anchor.y <= bounds.maxY
117
+ return inscribedRadius > 0 && anchorInside ? inscribedRadius : hypot(bounds.width, bounds.height) / 2
118
+ }
119
+
120
+ // Scale at which the shape is startHeight points tall.
121
+ static func startScale(for shape: RevealShape) -> CGFloat {
122
+ startHeight / shape.bounds.height
123
+ }
124
+
125
+ static func shapePath(_ shape: RevealShape, origin: CGPoint, scale: CGFloat) -> CGPath {
126
+ let transform = CGAffineTransform(translationX: origin.x, y: origin.y)
127
+ .scaledBy(x: scale, y: scale)
128
+ .translatedBy(x: -shape.anchor.x, y: -shape.anchor.y)
129
+ let path = CGMutablePath()
130
+ path.addPath(shape.path, transform: transform)
131
+ return path
132
+ }
133
+
134
+ // reveal: the window rect with the shape as an even-odd hole. inverse: the shape alone.
135
+ static func maskPath(_ shape: RevealShape, origin: CGPoint, scale: CGFloat, bounds: CGRect, inverse: Bool) -> CGPath {
136
+ if inverse { return shapePath(shape, origin: origin, scale: scale) }
137
+ let path = CGMutablePath()
138
+ path.addRect(bounds)
139
+ path.addPath(shapePath(shape, origin: origin, scale: scale))
140
+ return path
141
+ }
142
+
143
+ static func sampleCount(duration: Double) -> Int {
144
+ guard duration > 0 else { return 2 }
145
+ return min(maxSamples, max(2, Int((min(duration, maxSeconds) * samplesPerSecond).rounded())))
146
+ }
147
+
148
+ // steps + 1 scales, g linear in time from 0 to 1.
149
+ static func scales(from: Double, to: Double, steps: Int, inverse: Bool) -> [Double] {
150
+ (0...steps).map { step in
151
+ let g = Double(step) / Double(steps)
152
+ return inverse ? shrink(g: g, from: from, to: to) : growth(g: g, from: from, to: to)
153
+ }
154
+ }
155
+
156
+ // How far the wipe mask moves. The edge travels in the named direction, uncovering the
157
+ // new theme behind it (up: from the bottom).
158
+ static func wipeOffset(_ direction: WipeDirection, size: CGSize) -> CGPoint {
159
+ switch direction {
160
+ case .up: return CGPoint(x: 0, y: -size.height)
161
+ case .down: return CGPoint(x: 0, y: size.height)
162
+ case .left: return CGPoint(x: -size.width, y: 0)
163
+ case .right: return CGPoint(x: size.width, y: 0)
164
+ }
165
+ }
166
+
167
+ private static func bezier(_ s: Double, _ p1: Double, _ p2: Double) -> Double {
168
+ let u = 1 - s
169
+ return 3 * u * u * s * p1 + 3 * u * s * s * p2 + s * s * s
170
+ }
171
+ }