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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/Aniview.d.ts +63 -0
- package/dist/Aniview.d.ts.map +1 -0
- package/dist/Aniview.js +831 -0
- package/dist/Aniview.js.map +1 -0
- package/dist/AniviewPanel.d.ts +33 -0
- package/dist/AniviewPanel.d.ts.map +1 -0
- package/dist/AniviewPanel.js +66 -0
- package/dist/AniviewPanel.js.map +1 -0
- package/dist/GestureStressTest.d.ts +3 -0
- package/dist/GestureStressTest.d.ts.map +1 -0
- package/dist/GestureStressTest.js +125 -0
- package/dist/GestureStressTest.js.map +1 -0
- package/dist/aniviewConfig.d.ts +175 -0
- package/dist/aniviewConfig.d.ts.map +1 -0
- package/dist/aniviewConfig.js +568 -0
- package/dist/aniviewConfig.js.map +1 -0
- package/dist/aniviewProvider.d.ts +93 -0
- package/dist/aniviewProvider.d.ts.map +1 -0
- package/dist/aniviewProvider.js +229 -0
- package/dist/aniviewProvider.js.map +1 -0
- package/dist/core/AniviewLock.d.ts +16 -0
- package/dist/core/AniviewLock.d.ts.map +1 -0
- package/dist/core/AniviewLock.js +18 -0
- package/dist/core/AniviewLock.js.map +1 -0
- package/dist/core/AniviewMath.d.ts +41 -0
- package/dist/core/AniviewMath.d.ts.map +1 -0
- package/dist/core/AniviewMath.js +69 -0
- package/dist/core/AniviewMath.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/useAniview.d.ts +39 -0
- package/dist/useAniview.d.ts.map +1 -0
- package/dist/useAniview.js +32 -0
- package/dist/useAniview.js.map +1 -0
- package/dist/useAniviewContext.d.ts +156 -0
- package/dist/useAniviewContext.d.ts.map +1 -0
- package/dist/useAniviewContext.js +3 -0
- package/dist/useAniviewContext.js.map +1 -0
- package/dist/useAniviewLock.d.ts +20 -0
- package/dist/useAniviewLock.d.ts.map +1 -0
- package/dist/useAniviewLock.js +32 -0
- package/dist/useAniviewLock.js.map +1 -0
- package/package.json +60 -0
- package/src/Aniview.tsx +868 -0
- package/src/AniviewPanel.tsx +141 -0
- package/src/GestureStressTest.tsx +144 -0
- package/src/__tests__/AniviewLock.test.ts +58 -0
- package/src/__tests__/AniviewMath.test.ts +211 -0
- package/src/__tests__/AniviewSnapshot.test.tsx +85 -0
- package/src/__tests__/__snapshots__/AniviewSnapshot.test.tsx.snap +7 -0
- package/src/__tests__/aniviewConfig.test.ts +70 -0
- package/src/aniviewConfig.tsx +688 -0
- package/src/aniviewProvider.tsx +307 -0
- package/src/core/AniviewLock.ts +23 -0
- package/src/core/AniviewMath.ts +107 -0
- package/src/index.ts +6 -0
- package/src/useAniview.tsx +75 -0
- package/src/useAniviewContext.tsx +170 -0
- 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);
|