aniview 1.0.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 (64) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/dist/Aniview.d.ts +63 -0
  5. package/dist/Aniview.d.ts.map +1 -0
  6. package/dist/Aniview.js +831 -0
  7. package/dist/Aniview.js.map +1 -0
  8. package/dist/AniviewPanel.d.ts +33 -0
  9. package/dist/AniviewPanel.d.ts.map +1 -0
  10. package/dist/AniviewPanel.js +66 -0
  11. package/dist/AniviewPanel.js.map +1 -0
  12. package/dist/GestureStressTest.d.ts +3 -0
  13. package/dist/GestureStressTest.d.ts.map +1 -0
  14. package/dist/GestureStressTest.js +125 -0
  15. package/dist/GestureStressTest.js.map +1 -0
  16. package/dist/aniviewConfig.d.ts +175 -0
  17. package/dist/aniviewConfig.d.ts.map +1 -0
  18. package/dist/aniviewConfig.js +568 -0
  19. package/dist/aniviewConfig.js.map +1 -0
  20. package/dist/aniviewProvider.d.ts +93 -0
  21. package/dist/aniviewProvider.d.ts.map +1 -0
  22. package/dist/aniviewProvider.js +229 -0
  23. package/dist/aniviewProvider.js.map +1 -0
  24. package/dist/core/AniviewLock.d.ts +16 -0
  25. package/dist/core/AniviewLock.d.ts.map +1 -0
  26. package/dist/core/AniviewLock.js +18 -0
  27. package/dist/core/AniviewLock.js.map +1 -0
  28. package/dist/core/AniviewMath.d.ts +41 -0
  29. package/dist/core/AniviewMath.d.ts.map +1 -0
  30. package/dist/core/AniviewMath.js +69 -0
  31. package/dist/core/AniviewMath.js.map +1 -0
  32. package/dist/index.d.ts +7 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +7 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/useAniview.d.ts +39 -0
  37. package/dist/useAniview.d.ts.map +1 -0
  38. package/dist/useAniview.js +32 -0
  39. package/dist/useAniview.js.map +1 -0
  40. package/dist/useAniviewContext.d.ts +156 -0
  41. package/dist/useAniviewContext.d.ts.map +1 -0
  42. package/dist/useAniviewContext.js +3 -0
  43. package/dist/useAniviewContext.js.map +1 -0
  44. package/dist/useAniviewLock.d.ts +20 -0
  45. package/dist/useAniviewLock.d.ts.map +1 -0
  46. package/dist/useAniviewLock.js +32 -0
  47. package/dist/useAniviewLock.js.map +1 -0
  48. package/package.json +60 -0
  49. package/src/Aniview.tsx +868 -0
  50. package/src/AniviewPanel.tsx +141 -0
  51. package/src/GestureStressTest.tsx +144 -0
  52. package/src/__tests__/AniviewLock.test.ts +58 -0
  53. package/src/__tests__/AniviewMath.test.ts +211 -0
  54. package/src/__tests__/AniviewSnapshot.test.tsx +85 -0
  55. package/src/__tests__/__snapshots__/AniviewSnapshot.test.tsx.snap +7 -0
  56. package/src/__tests__/aniviewConfig.test.ts +70 -0
  57. package/src/aniviewConfig.tsx +688 -0
  58. package/src/aniviewProvider.tsx +307 -0
  59. package/src/core/AniviewLock.ts +23 -0
  60. package/src/core/AniviewMath.ts +107 -0
  61. package/src/index.ts +6 -0
  62. package/src/useAniview.tsx +75 -0
  63. package/src/useAniviewContext.tsx +170 -0
  64. package/src/useAniviewLock.tsx +37 -0
@@ -0,0 +1,307 @@
1
+ import React, { useMemo, useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
2
+ import { LayoutChangeEvent, View, Dimensions as RNDimensions, LayoutRectangle } from 'react-native';
3
+ import Animated, { useSharedValue, withSpring, makeMutable, SharedValue, WithSpringConfig } from 'react-native-reanimated';
4
+ import { AniviewContext, AniviewContextType, AniviewHandle, AniviewFrame, BakedFrame, IAniviewConfig, WorldBounds } from "./useAniviewContext";
5
+ import { AniviewConfig } from './aniviewConfig';
6
+ import { GestureDetector, PanGesture } from 'react-native-gesture-handler';
7
+ import * as AniviewMath from './core/AniviewMath';
8
+
9
+ export interface AniviewProviderProps {
10
+ children: React.ReactNode;
11
+ /** Explicitly provide a config instance (Advanced) */
12
+ config?: AniviewConfig;
13
+ /** The grid layout matrix (e.g., [[1, 1, 1, 1]]) - Simple setup */
14
+ layout?: number[][];
15
+ /** The source-of-truth origin page */
16
+ defaultPage?: number;
17
+ /** Explicitly override page size. Defaults to container size. */
18
+ pageSize?: { width: number; height: number };
19
+ /** Callback when page changes */
20
+ onPageChange?: (pageId: number | string) => void;
21
+ /** Imperative active page control (Legacy) - prefer ref.snapToPage */
22
+ activePage?: number | string;
23
+ /** Map of semantic names to numeric page IDs */
24
+ pageMap?: Record<string, number>;
25
+ /** External ref for the internal PanGesture (for coordination) */
26
+ gestureRef?: React.RefObject<any>;
27
+ /** Custom spring physics */
28
+ springConfig?: WithSpringConfig;
29
+ /** External events to drive animations */
30
+ events?: Record<string, SharedValue<number>>;
31
+ /** Explicit dimensions override */
32
+ dimensions?: Partial<AniviewContextType['dimensions']>;
33
+ /** External lockMask for gesture coordination */
34
+ externalLockMask?: SharedValue<number>;
35
+ /** External gesture enabled control (simple on/off) */
36
+ gestureEnabled?: SharedValue<boolean>;
37
+ /** Simultaneous gesture handlers for coordination */
38
+ simultaneousHandlers?: any;
39
+ }
40
+
41
+ /**
42
+ * **AniviewProvider** — World Coordinate System & Gesture Controller
43
+ *
44
+ * The root provider that establishes the virtual 2D world in which all
45
+ * child `Aniview` components live. It manages:
46
+ *
47
+ * - **The Camera** (`events.x`, `events.y` SharedValues) — the current
48
+ * viewport position in world coordinates, driven by gestures or
49
+ * programmatic `snapToPage` calls.
50
+ * - **Pan Gesture** — a RNGH Pan gesture that translates finger movement
51
+ * into camera position updates, with axis locking, edge resistance,
52
+ * velocity-based snapping, and bitmask-based directional locks.
53
+ * - **Virtualization** — tracks which pages are "near" the camera and
54
+ * provides a `visiblePages` set for child components.
55
+ * - **Custom Events** — user-supplied SharedValues (e.g., scroll position,
56
+ * slider value) that drive event-based keyframe animations.
57
+ *
58
+ * ### Sizing behavior
59
+ *
60
+ * By default, the provider measures its own container via `onLayout`
61
+ * and uses that as the page size. Override with `pageSize` or
62
+ * `dimensions` if you need explicit control (e.g., for offscreen pages
63
+ * that are larger than the visible container).
64
+ *
65
+ * ### Imperative API
66
+ *
67
+ * Attach a `ref` to access `snapToPage(pageId)`, `getCurrentPage()`,
68
+ * and `lock(mask)` for programmatic navigation.
69
+ *
70
+ * @param props.config - Pre-built `AniviewConfig` instance (advanced). Mutually exclusive with `layout`.
71
+ * @param props.layout - Grid matrix (e.g., `[[1, 1, 1]]` for 3 horizontal pages). Creates an `AniviewConfig` internally.
72
+ * @param props.defaultPage - The initial page ID (default: `0`).
73
+ * @param props.pageMap - Semantic name → numeric ID mapping (e.g., `{ HOME: 0, SETTINGS: 1 }`).
74
+ * @param props.pageSize - Explicit page dimensions. If omitted, inferred from container layout.
75
+ * @param props.onPageChange - Fires when the camera snaps to a new page.
76
+ * @param props.springConfig - Custom spring physics for snap animations.
77
+ * @param props.events - Additional SharedValues that drive event-based keyframes.
78
+ * @param props.externalLockMask - Externally controlled bitmask SharedValue for gesture locking. See {@link useAniviewLock}.
79
+ * @param props.gestureEnabled - SharedValue to globally enable/disable the pan gesture.
80
+ * @param props.simultaneousHandlers - RNGH refs for gesture coordination with parent/sibling handlers.
81
+ * @param props.activePage - (Legacy) Declarative page control. Prefer `ref.snapToPage()`.
82
+ *
83
+ * @example
84
+ * ```tsx
85
+ * const config = new AniviewConfig([[1, 1, 1]], 0, { HOME: 0, FEED: 1, PROFILE: 2 });
86
+ * const scrollY = useSharedValue(0);
87
+ *
88
+ * <AniviewProvider ref={aniviewRef} config={config} events={{ scrollY }} onPageChange={setPage}>
89
+ * <Aniview pageId="HOME">...</Aniview>
90
+ * <Aniview pageId="FEED">...</Aniview>
91
+ * </AniviewProvider>
92
+ * ```
93
+ */
94
+ export const AniviewProvider = forwardRef<AniviewHandle, AniviewProviderProps>(({
95
+ children,
96
+ config: providedConfig,
97
+ layout,
98
+ defaultPage = 0,
99
+ pageSize,
100
+ onPageChange,
101
+ activePage,
102
+ gestureRef,
103
+ springConfig,
104
+ events: externalEvents,
105
+ pageMap = {},
106
+ dimensions: providedDims,
107
+ externalLockMask,
108
+ gestureEnabled: externalGestureEnabled,
109
+ simultaneousHandlers
110
+ }: AniviewProviderProps, ref: React.Ref<AniviewHandle>) => {
111
+ // --- CONFIG MANAGEMENT ---
112
+ const config = useMemo(() => {
113
+ if (providedConfig) return providedConfig;
114
+ if (layout) return new AniviewConfig(layout, defaultPage, pageMap, {}, {}, {});
115
+ return new AniviewConfig([[1]], 0, pageMap, {}, {}, {}); // Fallback
116
+ }, [providedConfig, layout, defaultPage, pageMap]);
117
+
118
+ // --- STATE & PHYSICS ---
119
+ const x = useSharedValue(0);
120
+ const y = useSharedValue(0);
121
+
122
+ // Use external lockMask if provided, otherwise create internal one
123
+ const internalLockMask = useSharedValue(0);
124
+ const lockMask = externalLockMask || internalLockMask;
125
+
126
+ // Use external gestureEnabled if provided, otherwise create internal one
127
+ const internalGestureEnabled = useSharedValue(true);
128
+ const gestureEnabled = externalGestureEnabled || internalGestureEnabled;
129
+ const lastTargetId = useSharedValue<number | string>(config.defaultPage);
130
+ const isMoving = useSharedValue(false);
131
+
132
+ const [dimensions, setDimensions] = useState<AniviewContextType['dimensions']>({
133
+ width: providedDims?.width ?? pageSize?.width ?? 0,
134
+ height: providedDims?.height ?? pageSize?.height ?? 0,
135
+ offsetX: providedDims?.offsetX ?? 0,
136
+ offsetY: providedDims?.offsetY ?? 0
137
+ });
138
+
139
+ // --- VIRTUALIZATION STATE ---
140
+ // Tracks which pages are near the "viewport" on the JS thread
141
+ // We use a ref and a forced refresh ONLY when absolutely needed to prevent
142
+ // every Aniview component from re-rendering on every page traversal.
143
+ const visiblePagesRef = useRef<Set<number>>(new Set([config.resolvePageId(defaultPage)]));
144
+
145
+ const updateVisibility = (centerPage: number | string) => {
146
+ const visible = new Set<number>();
147
+ const pages = config.getPages();
148
+ const resolvedCenter = config.resolvePageId(centerPage);
149
+ const centerPos = AniviewMath.pageIdToMatrixPos(resolvedCenter, (config as any).layout);
150
+
151
+ pages.forEach((p: number) => {
152
+ const pPos = AniviewMath.pageIdToMatrixPos(p, (config as any).layout);
153
+ const rowDist = Math.abs(pPos.r - centerPos.r);
154
+ const colDist = Math.abs(pPos.c - centerPos.c);
155
+ if (rowDist <= 1 && colDist <= 1) visible.add(p);
156
+ });
157
+
158
+ const current = visiblePagesRef.current;
159
+ if (current.size !== visible.size || ![...visible].every(v => current.has(v))) {
160
+ visiblePagesRef.current = visible;
161
+ // Optimization: NO BROADCAST.
162
+ // We rely on the native proxy for virtualization checks within worklets.
163
+ }
164
+ };
165
+
166
+ // Sync the SharedValue target into the config engine for virtualization worklets
167
+ useEffect(() => {
168
+ if ((config as any)._setCurrentPageSV) {
169
+ (config as any)._setCurrentPageSV(lastTargetId);
170
+ }
171
+ }, [config, lastTargetId]);
172
+
173
+ // --- IMPERATIVE API ---
174
+ useImperativeHandle(ref, () => ({
175
+ snapToPage: (pageId: number | string) => {
176
+ const offset = config.getPageOffset(pageId, dimensions);
177
+ const springConfig = config.getSpringConfig();
178
+
179
+ isMoving.value = true;
180
+ x.value = withSpring(offset.x, springConfig, (finished) => {
181
+ if (finished) isMoving.value = false;
182
+ });
183
+ y.value = withSpring(offset.y, springConfig);
184
+
185
+ lastTargetId.value = pageId;
186
+ updateVisibility(pageId);
187
+ if (onPageChange) onPageChange(pageId);
188
+ },
189
+ getCurrentPage: () => lastTargetId.value,
190
+ lock: (mask: number) => { lockMask.value = mask; }
191
+ }));
192
+
193
+ // --- DYNAMIC UPDATES ---
194
+ useEffect(() => {
195
+ if (springConfig) config.updateSpringConfig(springConfig);
196
+ }, [springConfig, config]);
197
+
198
+ useEffect(() => {
199
+ config.updateDimensions(dimensions);
200
+ }, [dimensions, config]);
201
+
202
+ // --- ONLAYOUT AUTO-SIZING ---
203
+ const onLayout = (e: LayoutChangeEvent) => {
204
+ const { width, height, x: layoutX, y: layoutY } = e.nativeEvent.layout;
205
+
206
+ // Only update if dimensions drastically changed (debounce/diff)
207
+ if (
208
+ Math.abs(width - dimensions.width) > 1 ||
209
+ Math.abs(height - dimensions.height) > 1 ||
210
+ Math.abs(layoutX - dimensions.offsetX) > 1 ||
211
+ Math.abs(layoutY - dimensions.offsetY) > 1
212
+ ) {
213
+ setDimensions(prev => ({
214
+ ...prev,
215
+ width: providedDims?.width ?? (pageSize?.width || width),
216
+ height: providedDims?.height ?? (pageSize?.height || height),
217
+ offsetX: layoutX,
218
+ offsetY: layoutY
219
+ }));
220
+ }
221
+ };
222
+
223
+ // --- VIRTUALIZATION ---
224
+ const activationMap = useMemo(() => {
225
+ const map: Record<number, any> = {};
226
+ config.getPages().forEach(id => {
227
+ map[id] = makeMutable(1);
228
+ });
229
+ return map;
230
+ }, [config]);
231
+
232
+ // --- BOOTSTRAPPING ---
233
+ useEffect(() => {
234
+ if (config) {
235
+ const offset = config.getPageOffset(config.defaultPage, dimensions);
236
+ x.value = offset.x;
237
+ y.value = offset.y;
238
+ lastTargetId.value = config.defaultPage;
239
+ }
240
+ }, [config]);
241
+
242
+ // --- LEGACY TAB SYNC ---
243
+ useEffect(() => {
244
+ if (config && activePage !== undefined) {
245
+ if (lastTargetId.value === activePage) return;
246
+ const offset = config.getPageOffset(activePage, dimensions);
247
+ const isMoved = Math.abs(x.value - offset.x) > 1 || Math.abs(y.value - offset.y) > 1;
248
+
249
+ if (isMoved) {
250
+ const physics = config.getSpringConfig();
251
+ isMoving.value = true;
252
+ x.value = withSpring(offset.x, physics, (finished) => {
253
+ if (finished) isMoving.value = false;
254
+ });
255
+ y.value = withSpring(offset.y, physics);
256
+ lastTargetId.value = activePage;
257
+ }
258
+ }
259
+ }, [activePage, config]);
260
+
261
+ // --- GESTURE ---
262
+ // We recreate the gesture when key dependencies change.
263
+ // Ideally this should be stable, but config.generateGesture needs latest dims logic if re-run.
264
+ const panGesture = useMemo(() => {
265
+ return config.generateGesture(
266
+ x,
267
+ y,
268
+ (pageId) => {
269
+ lastTargetId.value = pageId;
270
+ updateVisibility(pageId);
271
+ if (onPageChange) onPageChange(pageId);
272
+ },
273
+ lockMask,
274
+ simultaneousHandlers,
275
+ gestureEnabled,
276
+ dimensions,
277
+ isMoving,
278
+ lastTargetId
279
+ );
280
+ }, [config, x, y, lockMask, onPageChange, dimensions, isMoving]); // Force rebuild when dimensions hit bitumen
281
+
282
+ if (gestureRef) {
283
+ (panGesture as any).ref = gestureRef;
284
+ }
285
+
286
+ const contextValue = useMemo(() => ({
287
+ dimensions,
288
+ events: { x, y, ...(externalEvents || {}) },
289
+ activationMap,
290
+ panGesture,
291
+ config,
292
+ lock: (mask: number) => { lockMask.value = mask; },
293
+ visiblePages: visiblePagesRef.current,
294
+ isMoving
295
+ }), [dimensions, x, y, externalEvents, activationMap, config, panGesture, lockMask]);
296
+
297
+ return (
298
+ <AniviewContext.Provider value={contextValue}>
299
+ <GestureDetector gesture={panGesture}>
300
+ {/* The Stage */}
301
+ <Animated.View style={{ flex: 1 }} onLayout={onLayout}>
302
+ {children}
303
+ </Animated.View>
304
+ </GestureDetector>
305
+ </AniviewContext.Provider>
306
+ );
307
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Valid directions for locking Aniview gestures.
3
+ */
4
+ export type AniviewAxisLock = {
5
+ left?: boolean;
6
+ right?: boolean;
7
+ up?: boolean;
8
+ down?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Utility to generate Aniview lock bitmasks.
13
+ */
14
+ export const AniviewLock = {
15
+ mask: (directions: AniviewAxisLock) => {
16
+ let mask = 0;
17
+ if (directions.left) mask |= 1;
18
+ if (directions.right) mask |= 2;
19
+ if (directions.up) mask |= 4;
20
+ if (directions.down) mask |= 8;
21
+ return mask;
22
+ }
23
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * ANIVIEW MATH CORE
3
+ *
4
+ * Pure functional implementation of Aniview's spatial logic.
5
+ */
6
+
7
+ export interface MatrixPos {
8
+ r: number;
9
+ c: number;
10
+ }
11
+
12
+ export interface WorldBounds {
13
+ minX: number;
14
+ maxX: number;
15
+ minY: number;
16
+ maxY: number;
17
+ }
18
+
19
+ /**
20
+ * Converts a linear PageID to a (Row, Column) matrix position.
21
+ */
22
+ export function pageIdToMatrixPos(pageId: number, layout: number[][]): MatrixPos {
23
+ 'worklet';
24
+ const rowLength = Math.max(1, layout[0]?.length || 0);
25
+ const numRows = layout.length;
26
+ const maxPage = numRows * rowLength - 1;
27
+
28
+ if (pageId < 0 || pageId > maxPage) {
29
+ return { r: 9999, c: 9999 };
30
+ }
31
+
32
+ return {
33
+ r: Math.floor(pageId / rowLength),
34
+ c: pageId % rowLength
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Calculates the displacement between two indices on an axis, accounting for overlaps.
40
+ */
41
+ export function calculateAxisOffset(
42
+ from: number,
43
+ to: number,
44
+ size: number,
45
+ overlaps: number[]
46
+ ): number {
47
+ 'worklet';
48
+ if (from === to) return 0;
49
+ const direction = to > from ? 1 : -1;
50
+ let totalOffset = 0;
51
+ const start = Math.min(from, to);
52
+ const end = Math.max(from, to);
53
+ for (let i = start; i < end; i++) {
54
+ const overlapRatio = overlaps[i] || 0;
55
+ totalOffset += size * (1 - overlapRatio);
56
+ }
57
+ return totalOffset * direction;
58
+ }
59
+
60
+ /**
61
+ * Calculates the (x, y) coordinates of a page relative to a default origin page.
62
+ */
63
+ export function getPageOffset(
64
+ pageId: number,
65
+ layout: number[][],
66
+ contextDims: { width: number; height: number },
67
+ defaultPage: number,
68
+ rowOverlaps: number[],
69
+ colOverlaps: number[]
70
+ ): { x: number; y: number } {
71
+ 'worklet';
72
+ const target = pageIdToMatrixPos(pageId, layout);
73
+ const origin = pageIdToMatrixPos(defaultPage, layout);
74
+
75
+ return {
76
+ x: calculateAxisOffset(origin.c, target.c, contextDims.width, colOverlaps),
77
+ y: calculateAxisOffset(origin.r, target.r, contextDims.height, rowOverlaps)
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Calculates the absolute boundaries of the virtual world.
83
+ */
84
+ export function getWorldBounds(
85
+ pages: number[],
86
+ layout: number[][],
87
+ contextDims: { width: number; height: number },
88
+ defaultPage: number,
89
+ rowOverlaps: number[],
90
+ colOverlaps: number[]
91
+ ): WorldBounds {
92
+ 'worklet';
93
+ if (pages.length === 0) return { minX: 0, maxX: 0, minY: 0, maxY: 0 };
94
+
95
+ const firstOffset = getPageOffset(pages[0], layout, contextDims, defaultPage, rowOverlaps, colOverlaps);
96
+ let minX = firstOffset.x, maxX = firstOffset.x, minY = firstOffset.y, maxY = firstOffset.y;
97
+
98
+ for (let i = 1; i < pages.length; i++) {
99
+ const offset = getPageOffset(pages[i], layout, contextDims, defaultPage, rowOverlaps, colOverlaps);
100
+ minX = Math.min(minX, offset.x);
101
+ maxX = Math.max(maxX, offset.x);
102
+ minY = Math.min(minY, offset.y);
103
+ maxY = Math.max(maxY, offset.y);
104
+ }
105
+
106
+ return { minX, maxX, minY, maxY };
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { default as Aniview } from './Aniview';
2
+ export { AniviewProvider } from './aniviewProvider';
3
+ export { AniviewConfig } from './aniviewConfig';
4
+ export { useAniview } from './useAniview';
5
+ export { useAniviewLock, AniviewLock } from './useAniviewLock';
6
+ export * from './useAniviewContext';
@@ -0,0 +1,75 @@
1
+ import * as React from 'react';
2
+ import { useContext } from 'react';
3
+ import { AniviewContext, AniviewProps, AniviewLogic, AniviewContextType } from "./useAniviewContext";
4
+
5
+ /**
6
+ * **useAniview** — Access the Aniview world context
7
+ *
8
+ * This hook has two overloads:
9
+ *
10
+ * **1. Context-only** (`useAniview()`) — Returns the raw {@link AniviewContextType}
11
+ * containing the camera SharedValues, config, dimensions, and gesture state.
12
+ * Use this when you need to read the camera position or react to page changes
13
+ * without being an animated component yourself.
14
+ *
15
+ * **2. Per-component** (`useAniview(props)`) — Used internally by `Aniview`
16
+ * components. Registers the component's `pageId` with the config engine and
17
+ * returns an {@link AniviewLogic} object containing the resolved registration,
18
+ * activation value, and keyframes. You rarely need this overload directly.
19
+ *
20
+ * @returns When called without arguments: `{ dimensions, events, config, activationMap, panGesture, visiblePages, isMoving }`
21
+ * @returns When called with props: `AniviewLogic` with additional `registration`, `activationValue`, and `keyframes`
22
+ *
23
+ * @throws Error if called outside of an `<AniviewProvider />`
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * // Read camera position from any child
28
+ * function CameraDebug() {
29
+ * const { events } = useAniview();
30
+ * // events.x.value, events.y.value are the camera world coordinates
31
+ * }
32
+ *
33
+ * // React to custom events
34
+ * function ScrollReactor() {
35
+ * const { events } = useAniview();
36
+ * // events.scrollY?.value (if parent passed events={{ scrollY }})
37
+ * }
38
+ * ```
39
+ */
40
+ export function useAniview(): AniviewContextType;
41
+ export function useAniview(props: AniviewProps): AniviewLogic;
42
+ export function useAniview(props?: AniviewProps): AniviewContextType | AniviewLogic {
43
+ const context = useContext(AniviewContext);
44
+ if (!context) {
45
+ throw new Error('Aniview components must be wrapped in an <AniviewProvider />');
46
+ }
47
+
48
+ // If no props provided, just return the raw context
49
+ if (!props || typeof props !== 'object') return context;
50
+
51
+ const pageId = props.pageId;
52
+
53
+ const registration = context.config ? context.config.registerPage(pageId, context.dimensions) : {
54
+ offset: { x: 0, y: 0 },
55
+ dimensions: context.dimensions
56
+ };
57
+
58
+ const activationValue = (context.activationMap && pageId !== undefined) ? (context.activationMap as any)[pageId] : null;
59
+
60
+ const logic: AniviewLogic = {
61
+ dimensions: context.dimensions,
62
+ events: context.events,
63
+ activationMap: context.activationMap,
64
+ panGesture: context.panGesture,
65
+ config: context.config,
66
+ registration: registration as any,
67
+ activationValue: activationValue as any,
68
+ keyframes: props.frames as any,
69
+ lock: context.lock,
70
+ visiblePages: context.visiblePages,
71
+ isMoving: context.isMoving,
72
+ };
73
+
74
+ return logic;
75
+ }
@@ -0,0 +1,170 @@
1
+ import React, { createContext } from "react";
2
+ import { SharedValue, AnimatedProps, WithSpringConfig } from "react-native-reanimated";
3
+ import { PanGesture } from "react-native-gesture-handler";
4
+ import { ViewProps, ViewStyle } from "react-native";
5
+ import { WorldBounds } from "./core/AniviewMath";
6
+
7
+ export type { WorldBounds };
8
+
9
+ /**
10
+ * ANIVIEW DOMAIN TYPES
11
+ */
12
+ export interface AniviewFrame {
13
+ /** Target page ID for spatial transitions (can be numeric index or semantic name) */
14
+ page?: number | string;
15
+ /** Custom event name for state-driven transitions (e.g., 'zoom', 'tilt') */
16
+ event?: string;
17
+ /** The value of the event driver that triggers this frame */
18
+ value?: number;
19
+ /** Style overrides for this frame */
20
+ style?: ViewStyle | ViewStyle[];
21
+ /**
22
+ * If true, this event-driven frame remains active across all pages.
23
+ * If false (default), the effect is modulated by proximity to the component's home page.
24
+ */
25
+ persistent?: boolean;
26
+ /** Optional opacity override shortcut */
27
+ opacity?: number;
28
+ /** Optional scale override shortcut */
29
+ scale?: number;
30
+ /** Optional rotate (Z) override shortcut (degrees) */
31
+ rotate?: number;
32
+ /** Spring config override for this specific frame transition (future) */
33
+ springConfig?: any;
34
+ }
35
+
36
+ export interface BakedFrame extends AniviewFrame {
37
+ worldX: number;
38
+ worldY: number;
39
+ }
40
+
41
+ export interface AniviewRegistration {
42
+ offset: { x: number; y: number };
43
+ dimensions: { width: number; height: number };
44
+ }
45
+
46
+ export interface AniviewProps extends AnimatedProps<ViewProps> {
47
+ pageId: number | string; // The ID of the page this component belongs to
48
+ frames?: AniviewFrame[] | Record<string, AniviewFrame>; // Supports both Array (New) and Object (Legacy)
49
+ events?: {
50
+ x: SharedValue<number>;
51
+ y: SharedValue<number>;
52
+ [key: string]: SharedValue<number>;
53
+ } | null;
54
+ children?: React.ReactNode;
55
+ style?: ViewStyle | ViewStyle[];
56
+ pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto';
57
+ /**
58
+ * If true, this component stays mounted even when offscreen.
59
+ * Essential for 3D/GL contexts.
60
+ * If false (default), it unmounts when offscreen to save RAM.
61
+ */
62
+ persistent?: boolean;
63
+ }
64
+
65
+ /**
66
+ * Imperative Handle for controlling the Aniview Physics Engine
67
+ */
68
+ export interface AniviewHandle {
69
+ /** Programmatically snap to a specific page */
70
+ snapToPage: (pageId: number | string) => void;
71
+ /** Get the current active page ID */
72
+ getCurrentPage: () => number | string;
73
+ /** Lock specific axes (bitmask: 1=Left, 2=Right, 4=Up, 8=Down) */
74
+ lock: (mask: number) => void;
75
+ }
76
+
77
+ /**
78
+ * Interface for the Aniview configuration engine.
79
+ */
80
+ export interface IAniviewConfig {
81
+ /** Gets the global (x, y) offset for a specific page relative to origin. */
82
+ getPageOffset(pageId: number | string, dims: AniviewContextType['dimensions']): { x: number; y: number };
83
+
84
+ /**
85
+ * Pre-calculates absolute coordinates for a component's keyframes.
86
+ * Executed during the 'Baking' phase on the JS thread.
87
+ */
88
+ register(
89
+ pageId: number | string,
90
+ dims: AniviewContextType['dimensions'],
91
+ keyframes?: AniviewFrame[] | Record<string, AniviewFrame>,
92
+ localLayout?: { x: number; y: number }
93
+ ): {
94
+ homeOffset: { x: number; y: number };
95
+ bakedFrames: Record<string, BakedFrame>;
96
+ eventLanes: Record<string, BakedFrame[]>; // Keyed by event name
97
+ localLayout: { x: number; y: number };
98
+ };
99
+
100
+ /** Minimal offset context for direct page references */
101
+ registerPage(pageId: number | string, dims: AniviewContextType['dimensions']): AniviewRegistration;
102
+
103
+ /** Returns all valid page IDs defined in the layout matrix */
104
+ getPages(): number[];
105
+
106
+ /** Map of PageID -> (x, y) coordinates */
107
+ getPagesMap(dims: AniviewContextType['dimensions']): Record<number, { x: number; y: number }>;
108
+
109
+ /** Returns calculated min/max world boundaries for gesture clamping */
110
+ getWorldBounds(dims: AniviewContextType['dimensions']): WorldBounds;
111
+
112
+ /** Update dimensions dynamically (used by onLayout) */
113
+ updateDimensions(dims: AniviewContextType['dimensions']): void;
114
+
115
+ /** Update spring config dynamically */
116
+ updateSpringConfig(config: WithSpringConfig): void;
117
+
118
+ /** Generates the core Pan Gesture logic */
119
+ generateGesture(
120
+ x: SharedValue<number>,
121
+ y: SharedValue<number>,
122
+ onPageChange?: (pageId: number | string) => void,
123
+ lockMask?: SharedValue<number>,
124
+ simultaneousHandlers?: any,
125
+ gestureEnabled?: SharedValue<boolean>,
126
+ dims?: AniviewContextType['dimensions'],
127
+ isSnapping?: SharedValue<boolean>,
128
+ lastTargetId?: SharedValue<number | string>
129
+ ): any;
130
+
131
+ /** Resolves a potentially semantic pageId string into its numeric index */
132
+ resolvePageId(pageId: number | string): number;
133
+
134
+ /** Returns the current active page ID (SharedValue) */
135
+ getCurrentPage(): SharedValue<number | string>;
136
+ }
137
+
138
+ export interface AniviewContextType {
139
+ dimensions: {
140
+ width: number;
141
+ height: number;
142
+ offsetX: number;
143
+ offsetY: number;
144
+ }
145
+ events: {
146
+ x: SharedValue<number>;
147
+ y: SharedValue<number>;
148
+ [key: string]: SharedValue<number>;
149
+ }
150
+ activationMap: Record<number, SharedValue<number>>;
151
+ panGesture: any;
152
+ config: IAniviewConfig;
153
+ /** Programmatically lock specific axes */
154
+ lock: (mask: number) => void;
155
+ /** Set of page IDs that should currently be mounted */
156
+ visiblePages: Set<number>;
157
+ /** Tracks if Aniview is currently snapping/animating toward a page */
158
+ isMoving: SharedValue<boolean>;
159
+ }
160
+
161
+ /**
162
+ * The full logic state for a specific Aniview component.
163
+ */
164
+ export interface AniviewLogic extends AniviewContextType {
165
+ registration: AniviewRegistration;
166
+ activationValue: SharedValue<number>;
167
+ keyframes: Record<string, AniviewFrame> | undefined;
168
+ }
169
+
170
+ export const AniviewContext = createContext<AniviewContextType | null>(null);