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,831 @@
1
+ import React, { useState, useCallback, useMemo, useEffect } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import Animated, { useAnimatedStyle, interpolate, interpolateColor, Extrapolation, useAnimatedReaction, runOnJS } from 'react-native-reanimated';
4
+ import { useAniview } from './useAniview';
5
+ /**
6
+ * HELPER: Strips mapping styles and provides transform array for reconstruction.
7
+ */
8
+ function stripLayoutProps(style) {
9
+ const flattened = StyleSheet.flatten(style) || {};
10
+ const { transform, position, left, top, right, bottom, marginLeft, marginTop, marginRight, marginBottom, ...rest } = flattened;
11
+ return { rest, transform: Array.isArray(transform) ? transform : [] };
12
+ }
13
+ /**
14
+ * HELPER: Flattens transform array into O(1) property map for interpolation.
15
+ */
16
+ function flattenTransform(transform) {
17
+ const props = {};
18
+ transform.forEach(t => {
19
+ const key = Object.keys(t)[0];
20
+ let val = t[key];
21
+ // Handle rotation strings
22
+ if (typeof val === 'string' && val.endsWith('deg'))
23
+ val = parseFloat(val);
24
+ props[`_tr_${key}`] = val;
25
+ });
26
+ return props;
27
+ }
28
+ /**
29
+ * HELPER: Normalizes any color string to RGBA for consistent interpolation.
30
+ */
31
+ function normalizeColorToRgba(c) {
32
+ if (!c || c === 'transparent')
33
+ return 'rgba(0,0,0,0)';
34
+ if (c.startsWith('#')) {
35
+ const hex = c.slice(1);
36
+ let r = 0, g = 0, b = 0, a = 1;
37
+ if (hex.length === 3) {
38
+ r = parseInt(hex[0] + hex[0], 16);
39
+ g = parseInt(hex[1] + hex[1], 16);
40
+ b = parseInt(hex[2] + hex[2], 16);
41
+ }
42
+ else {
43
+ r = parseInt(hex.slice(0, 2), 16);
44
+ g = parseInt(hex.slice(2, 4), 16);
45
+ b = parseInt(hex.slice(4, 6), 16);
46
+ if (hex.length === 8)
47
+ a = parseInt(hex.slice(6, 8), 16) / 255;
48
+ }
49
+ return `rgba(${r},${g},${b},${a})`;
50
+ }
51
+ const match = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
52
+ if (match)
53
+ return `rgba(${match[1]},${match[2]},${match[3]},${match[4] || 1})`;
54
+ const hMatch = c.match(/hsla?\((\d+),\s*([\d.]+)%?,\s*([\d.]+)%?(?:,\s*([\d.]+))?\)/);
55
+ if (hMatch) {
56
+ const h = parseInt(hMatch[1]), s = parseFloat(hMatch[2]) / 100, l = parseFloat(hMatch[3]) / 100, a = hMatch[4] ? parseFloat(hMatch[4]) : 1;
57
+ const k = (n) => (n + h / 30) % 12;
58
+ const arc = s * Math.min(l, 1 - l);
59
+ const f = (n) => l - arc * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
60
+ return `rgba(${Math.round(255 * f(0))},${Math.round(255 * f(8))},${Math.round(255 * f(4))},${a})`;
61
+ }
62
+ return 'rgba(0,0,0,0)';
63
+ }
64
+ /**
65
+ * PURE JS COLOR INTERPOLATOR
66
+ */
67
+ function jsInterpolateColor(val, start, end, startColor, endColor) {
68
+ 'worklet';
69
+ if (!startColor || !endColor || startColor === endColor)
70
+ return startColor || 'rgba(0,0,0,0)';
71
+ const range = (end - start) || 1;
72
+ const progress = Math.max(0, Math.min(1, (val - start) / range));
73
+ const parse = (c) => {
74
+ if (!c || c === 'transparent')
75
+ return [0, 0, 0, 0];
76
+ if (c.startsWith('#')) {
77
+ const hex = c.slice(1);
78
+ if (hex.length === 3)
79
+ return [parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16), 1];
80
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16), hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1];
81
+ }
82
+ const match = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
83
+ if (match)
84
+ return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3]), match[4] ? parseFloat(match[4]) : 1];
85
+ const hMatch = c.match(/hsla?\((\d+),\s*([\d.]+)%?,\s*([\d.]+)%?(?:,\s*([\d.]+))?\)/);
86
+ if (hMatch) {
87
+ const h = parseInt(hMatch[1]), s = parseFloat(hMatch[2]) / 100, l = parseFloat(hMatch[3]) / 100, a = hMatch[4] ? parseFloat(hMatch[4]) : 1;
88
+ const k = (n) => (n + h / 30) % 12;
89
+ const arc = s * Math.min(l, 1 - l);
90
+ const f = (n) => l - arc * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
91
+ return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4)), a];
92
+ }
93
+ return [0, 0, 0, 0];
94
+ };
95
+ const s = parse(startColor), e = parse(endColor);
96
+ const r = Math.round(s[0] + (e[0] - s[0]) * progress);
97
+ const g = Math.round(s[1] + (e[1] - s[1]) * progress);
98
+ const b = Math.round(s[2] + (e[2] - s[2]) * progress);
99
+ const a = s[3] + (e[3] - s[3]) * progress;
100
+ return `rgba(${r},${g},${b},${a})`;
101
+ }
102
+ /**
103
+ * SMART INTERPOLATE COLOR
104
+ * Handles "transparent" interpolation by adopting the RGB of the non-transparent color
105
+ * to avoid darkening/graying artifacts.
106
+ */
107
+ function smartInterpolateColor(val, input, output) {
108
+ 'worklet';
109
+ if (output.length === 0)
110
+ return 'rgba(0,0,0,0)';
111
+ if (output.length === 1)
112
+ return output[0];
113
+ // Bounds check
114
+ if (val <= input[0])
115
+ return output[0];
116
+ if (val >= input[input.length - 1])
117
+ return output[output.length - 1];
118
+ // Find segment
119
+ let i = 0;
120
+ // Iterate to find the correct segment
121
+ while (i < input.length - 2 && val >= input[i + 1]) {
122
+ i++;
123
+ }
124
+ // Safety check
125
+ if (input[i + 1] === undefined)
126
+ return output[output.length - 1];
127
+ const c1 = output[i];
128
+ const c2 = output[i + 1];
129
+ let useC1 = c1;
130
+ let useC2 = c2;
131
+ const fix = (transp, source) => {
132
+ 'worklet';
133
+ // We target normalized 'rgba(0,0,0,0)'
134
+ if (transp !== 'rgba(0,0,0,0)')
135
+ return transp;
136
+ if (!source || !source.startsWith('rgba'))
137
+ return transp;
138
+ const lastComma = source.lastIndexOf(',');
139
+ if (lastComma === -1)
140
+ return transp;
141
+ // Construct: source RGB + alpha 0
142
+ return source.substring(0, lastComma + 1) + '0)';
143
+ };
144
+ useC1 = fix(c1, c2);
145
+ useC2 = fix(c2, c1);
146
+ return interpolateColor(val, [input[i], input[i + 1]], [useC1, useC2]);
147
+ }
148
+ /**
149
+ * OPTIMIZATION: One-time Segment Data Search
150
+ * Finds the segment index and progress percentage once per frame so we
151
+ * don't have to perform a search for every single style property.
152
+ */
153
+ function getSegmentInfo(val, input) {
154
+ 'worklet';
155
+ const len = input.length;
156
+ if (len === 0)
157
+ return { i: 0, p: 0, constant: true };
158
+ if (len === 1 || val <= input[0])
159
+ return { i: 0, p: 0, constant: true };
160
+ if (val >= input[len - 1])
161
+ return { i: len - 1, p: 0, constant: true };
162
+ let i = 0;
163
+ while (i < len - 1 && val >= input[i + 1]) {
164
+ i++;
165
+ }
166
+ const gap = input[i + 1] - input[i];
167
+ const p = gap > 0.001 ? (val - input[i]) / gap : 0;
168
+ return { i, p, constant: false };
169
+ }
170
+ /**
171
+ * **Aniview** — Absolute World Coordinate Animation Engine Component
172
+ *
173
+ * The core animated view that interpolates its style properties based on
174
+ * the camera's position in a virtual 2D world coordinate system. Each
175
+ * `Aniview` belongs to a specific `pageId` (its "home page") and defines
176
+ * `frames` — keyframes that describe how the component should look when
177
+ * the camera is at a different page or when a custom event reaches a
178
+ * certain value.
179
+ *
180
+ * ### How it works (the "Baking" pipeline)
181
+ *
182
+ * 1. On mount (or when `frames`/`style`/`dimensions` change), Aniview
183
+ * runs a one-time **bake** on the JS thread. This pre-computes a
184
+ * lookup grid of style values indexed by world coordinates.
185
+ * 2. On every frame, `useAnimatedStyle` reads the camera SharedValues
186
+ * (`events.x`, `events.y`) and performs O(1) segment lookups +
187
+ * linear/color interpolation — no searching, no string parsing.
188
+ * 3. The baked data also powers **native virtualization**: components
189
+ * farther than 1.5× screen width from the camera are hidden via
190
+ * `opacity: 0`, keeping the render tree lightweight.
191
+ *
192
+ * ### Key design choices
193
+ *
194
+ * - **Absolute positioning**: All Aniview children are rendered with
195
+ * `position: 'absolute'` and placed via `translateX`/`translateY`
196
+ * relative to the camera. This decouples layout from animation.
197
+ * - **Smart color interpolation**: When transitioning to/from
198
+ * `transparent`, the RGB of the non-transparent color is preserved
199
+ * to avoid grey flash artifacts.
200
+ * - **Selective unmounting**: Non-persistent components unmount when
201
+ * offscreen (after snapping completes) to reclaim memory. Set
202
+ * `persistent={true}` for 3D/GL content that must stay mounted.
203
+ *
204
+ * @param props Standard `ViewProps` plus:
205
+ * @param props.pageId - The page this component belongs to (numeric index or semantic name from `pageMap`)
206
+ * @param props.frames - Keyframe definitions. Object keys are frame names, values are {@link AniviewFrame}
207
+ * @param props.persistent - If `true`, stays mounted even when far offscreen. Required for WebGL/Three.js canvases. Default: `false`
208
+ * @param props.style - Base styles applied when at home position. Numeric and color props here become the interpolation baseline.
209
+ * @param props.pointerEvents - Standard RN pointer events. Useful for overlay pages that shouldn't block touches.
210
+ *
211
+ * @example
212
+ * ```tsx
213
+ * <Aniview
214
+ * pageId="HOME"
215
+ * style={{ width: '100%', height: '100%', backgroundColor: '#fff' }}
216
+ * frames={{
217
+ * away: { page: 'SETTINGS', opacity: 0 },
218
+ * scrolled: { event: 'scrollY', value: 100, style: { transform: [{ translateY: -50 }] } },
219
+ * }}
220
+ * >
221
+ * <HomeContent />
222
+ * </Aniview>
223
+ * ```
224
+ *
225
+ * @see {@link AniviewProvider} for setting up the coordinate system
226
+ * @see {@link AniviewConfig} for layout and gesture configuration
227
+ * @see {@link useAniview} for accessing context from child components
228
+ */
229
+ export default function Aniview(props) {
230
+ const { style, children, pageId, frames, ...rest } = props;
231
+ const context = useAniview(props);
232
+ const { events, config, dimensions, isMoving } = context;
233
+ const persistent = props.persistent ?? false;
234
+ const [shouldRender, setShouldRender] = useState(true);
235
+ // Pre-calculate target X on the JS thread to avoid UI thread crash (calling class methods)
236
+ const homeX = useMemo(() => {
237
+ if (!config || !dimensions.width)
238
+ return 0;
239
+ const homeId = config.resolvePageId(pageId);
240
+ return config.getPageOffset(homeId, dimensions).x;
241
+ }, [config, pageId, dimensions.width, dimensions.height]);
242
+ // SELECTIVE UNMOUNTING: If not persistent, monitor visibility natively.
243
+ useAnimatedReaction(() => {
244
+ if (persistent || !isMoving || isMoving.value)
245
+ return null;
246
+ const cameraX = events.x.value;
247
+ const isFar = Math.abs(cameraX - homeX) > dimensions.width * 1.5;
248
+ return isFar;
249
+ }, (isFar) => {
250
+ if (isFar === null)
251
+ return null;
252
+ if (isFar && shouldRender)
253
+ runOnJS(setShouldRender)(false);
254
+ if (!isFar && !shouldRender)
255
+ runOnJS(setShouldRender)(true);
256
+ }, [persistent, dimensions.width, homeX, shouldRender, isMoving]);
257
+ const hasNoDims = dimensions.width <= 0 || dimensions.height <= 0;
258
+ const cached = useMemo(() => config?.getLayout(pageId.toString()), [config, pageId]);
259
+ const [localLayout, setLocalLayout] = useState(cached || null);
260
+ useEffect(() => {
261
+ if (localLayout && !cached) {
262
+ config?.registerLayout(pageId.toString(), localLayout);
263
+ }
264
+ }, [localLayout, cached, config, pageId]);
265
+ // FIX: Sync localLayout with controlled style changes
266
+ useEffect(() => {
267
+ const flat = StyleSheet.flatten(style || {});
268
+ const x = flat.left ?? flat.marginLeft ?? 0;
269
+ const y = flat.top ?? flat.marginTop ?? 0;
270
+ if (!localLayout || Math.abs(localLayout.x - x) > 0.1 || Math.abs(localLayout.y - y) > 0.1) {
271
+ setLocalLayout({ x, y });
272
+ }
273
+ }, [style, localLayout]);
274
+ const onLayout = useCallback((e) => {
275
+ let { x, y, width, height } = e.nativeEvent.layout;
276
+ const flattened = StyleSheet.flatten(props.style || {});
277
+ if (x === 0 && typeof flattened?.marginLeft === 'number')
278
+ x = flattened.marginLeft;
279
+ if (y === 0 && typeof flattened?.marginTop === 'number')
280
+ y = flattened.marginTop;
281
+ if (!localLayout || Math.abs(localLayout.x - x) > 1 || Math.abs(localLayout.y - y) > 1) {
282
+ if (width > 0 || height > 0) {
283
+ setLocalLayout({ x, y });
284
+ }
285
+ }
286
+ }, [localLayout, props.style]);
287
+ const baseStyle = useMemo(() => {
288
+ const flattened = StyleSheet.flatten(style) || {};
289
+ return { ...flattened, position: 'absolute' };
290
+ }, [style]);
291
+ // --- THE BAKE ---
292
+ const baked = useMemo(() => {
293
+ if (!localLayout || !config)
294
+ return null;
295
+ const isColorProp = (k) => k.toLowerCase().includes('color') || k === 'tintColor';
296
+ try {
297
+ const bakeInfo = config.register(pageId, dimensions, frames, localLayout);
298
+ const { homeOffset } = bakeInfo;
299
+ const homeX = homeOffset.x + localLayout.x;
300
+ const homeY = homeOffset.y + localLayout.y;
301
+ const baseOpacity = baseStyle.opacity ?? 1;
302
+ // 1. Initial State Extraction
303
+ const homeProps = { worldX: homeX, worldY: homeY, opacity: baseOpacity };
304
+ const flatBase = StyleSheet.flatten(style) || {};
305
+ for (const k in flatBase) {
306
+ const v = flatBase[k];
307
+ if (typeof v === 'number' || (typeof v === 'string' && isColorProp(k))) {
308
+ homeProps[k] = (typeof v === 'string' && isColorProp(k)) ? normalizeColorToRgba(v) : v;
309
+ }
310
+ else if (k === 'transform' && Array.isArray(v)) {
311
+ Object.assign(homeProps, flattenTransform(v));
312
+ }
313
+ else if (k === 'shadowOffset' || k === 'textShadowOffset') {
314
+ homeProps[k + 'Width'] = v?.width ?? 0;
315
+ homeProps[k + 'Height'] = v?.height ?? 0;
316
+ }
317
+ }
318
+ // 2. Pre-scan all frames to find ALL active keys and update homeProps
319
+ if (frames) {
320
+ const registration = bakeInfo;
321
+ const scanFrame = (frame) => {
322
+ if (frame.style) {
323
+ const fStyles = Array.isArray(frame.style) ? frame.style : [frame.style];
324
+ fStyles.forEach((s) => {
325
+ const fFlat = StyleSheet.flatten(s) || {};
326
+ for (const k in fFlat) {
327
+ const v = fFlat[k];
328
+ let key = k;
329
+ if (k === 'transform' && Array.isArray(v)) {
330
+ const flatTr = flattenTransform(v);
331
+ Object.keys(flatTr).forEach(tk => {
332
+ if (homeProps[tk] === undefined) {
333
+ homeProps[tk] = tk.includes('scale') ? 1 : 0;
334
+ }
335
+ });
336
+ }
337
+ else if (k === 'shadowOffset' || k === 'textShadowOffset') {
338
+ if (homeProps[k + 'Width'] === undefined) {
339
+ homeProps[k + 'Width'] = 0;
340
+ homeProps[k + 'Height'] = 0;
341
+ }
342
+ }
343
+ else if (homeProps[k] === undefined && (typeof v === 'number' || (typeof v === 'string' && isColorProp(k)))) {
344
+ homeProps[k] = (k === 'opacity' || k.includes('scale')) ? 1 : (isColorProp(k) ? 'rgba(0,0,0,0)' : 0);
345
+ }
346
+ }
347
+ });
348
+ }
349
+ if (frame.opacity !== undefined && homeProps.opacity === undefined)
350
+ homeProps.opacity = 1;
351
+ if (frame.scale !== undefined && homeProps._tr_scale === undefined)
352
+ homeProps._tr_scale = 1;
353
+ if (frame.rotate !== undefined && homeProps._tr_rotate === undefined)
354
+ homeProps._tr_rotate = 0;
355
+ };
356
+ for (const frameKey in registration.bakedFrames) {
357
+ scanFrame(registration.bakedFrames[frameKey]);
358
+ }
359
+ for (const laneKey in registration.eventLanes) {
360
+ registration.eventLanes[laneKey].forEach(scanFrame);
361
+ }
362
+ }
363
+ // 3. Spatial Grid Construction (2D)
364
+ const grid = { [`${homeOffset.x}_${homeOffset.y}`]: homeProps };
365
+ const uniqueX = new Set([homeOffset.x]);
366
+ const uniqueY = new Set([homeOffset.y]);
367
+ const activeKeys = new Set();
368
+ // 4. Event Lane Construction (1D)
369
+ const eventLanes = {};
370
+ if (frames) {
371
+ const registration = bakeInfo;
372
+ // Process Spatial Frames
373
+ for (const frameKey in registration.bakedFrames) {
374
+ const frame = registration.bakedFrames[frameKey];
375
+ const targetX = frame.worldX + homeOffset.x;
376
+ const targetY = frame.worldY + homeOffset.y;
377
+ uniqueX.add(targetX);
378
+ uniqueY.add(targetY);
379
+ // Start with a full copy of COMPLETE homeProps
380
+ const frameProps = { ...homeProps, worldX: targetX + localLayout.x, worldY: targetY + localLayout.y };
381
+ if (frame.style) {
382
+ const fStyles = Array.isArray(frame.style) ? frame.style : [frame.style];
383
+ fStyles.forEach(s => {
384
+ const fFlat = StyleSheet.flatten(s) || {};
385
+ for (const k in fFlat) {
386
+ const v = fFlat[k];
387
+ if (k === 'transform' && Array.isArray(v)) {
388
+ Object.assign(frameProps, flattenTransform(v));
389
+ }
390
+ else if (k === 'shadowOffset' || k === 'textShadowOffset') {
391
+ frameProps[k + 'Width'] = v?.width ?? 0;
392
+ frameProps[k + 'Height'] = v?.height ?? 0;
393
+ }
394
+ else if (typeof v === 'number' || (typeof v === 'string' && isColorProp(k))) {
395
+ let finalV = v;
396
+ if (typeof v === 'string' && isColorProp(k))
397
+ finalV = normalizeColorToRgba(v);
398
+ if (k === 'left' || k === 'marginLeft')
399
+ frameProps.worldX = targetX + Number(v) + (dimensions.offsetX || 0);
400
+ else if (k === 'top' || k === 'marginTop')
401
+ frameProps.worldY = targetY + Number(v) + (dimensions.offsetY || 0);
402
+ else
403
+ frameProps[k] = finalV;
404
+ // If this is a NEW key not in homeProps, add it to homeProps with a default
405
+ if (homeProps[k] === undefined) {
406
+ homeProps[k] = (k === 'opacity' || k.includes('scale')) ? 1 : (isColorProp(k) ? 'rgba(0,0,0,0)' : 0);
407
+ }
408
+ }
409
+ }
410
+ });
411
+ }
412
+ // Add shortcuts
413
+ if (frame.opacity !== undefined)
414
+ frameProps.opacity = frame.opacity;
415
+ if (frame.scale !== undefined)
416
+ frameProps._tr_scale = frame.scale;
417
+ if (frame.rotate !== undefined)
418
+ frameProps._tr_rotate = frame.rotate;
419
+ for (const p in frameProps) {
420
+ if (frameProps[p] !== homeProps[p])
421
+ activeKeys.add(p);
422
+ }
423
+ grid[`${targetX}_${targetY}`] = frameProps;
424
+ }
425
+ // Process Event Lanes
426
+ for (const eventName in registration.eventLanes) {
427
+ const framesInLane = registration.eventLanes[eventName];
428
+ const inputValues = framesInLane.map(f => f.value || 0);
429
+ const keysInLane = new Set();
430
+ framesInLane.forEach(f => {
431
+ const fStyles = Array.isArray(f.style) ? f.style : (f.style ? [f.style] : []);
432
+ fStyles.forEach(s => {
433
+ const flat = StyleSheet.flatten(s) || {};
434
+ Object.keys(flat).forEach(k => {
435
+ if (k === 'transform') {
436
+ const tr = flattenTransform(flat.transform);
437
+ Object.keys(tr).forEach(tk => keysInLane.add(tk));
438
+ }
439
+ else {
440
+ keysInLane.add(k);
441
+ }
442
+ });
443
+ });
444
+ if (f.opacity !== undefined)
445
+ keysInLane.add('opacity');
446
+ if (f.scale !== undefined)
447
+ keysInLane.add('_tr_scale');
448
+ if (f.rotate !== undefined)
449
+ keysInLane.add('_tr_rotate');
450
+ });
451
+ const keysArr = Array.from(keysInLane);
452
+ const outputRanges = keysArr.map(k => {
453
+ return framesInLane.map(f => {
454
+ const fStyles = Array.isArray(f.style) ? f.style : (f.style ? [f.style] : []);
455
+ let val = homeProps[k];
456
+ fStyles.forEach(s => {
457
+ const flat = StyleSheet.flatten(s) || {};
458
+ if (k.startsWith('_tr_')) {
459
+ const tr = flattenTransform(flat.transform || []);
460
+ if (tr[k] !== undefined)
461
+ val = tr[k];
462
+ }
463
+ else {
464
+ if (flat[k] !== undefined)
465
+ val = flat[k];
466
+ }
467
+ });
468
+ if (k === 'opacity' && f.opacity !== undefined)
469
+ val = f.opacity;
470
+ if (k === '_tr_scale' && f.scale !== undefined)
471
+ val = f.scale;
472
+ if (k === '_tr_rotate' && f.rotate !== undefined)
473
+ val = f.rotate;
474
+ return val;
475
+ });
476
+ });
477
+ const isPersistent = framesInLane.some(f => f.persistent === true);
478
+ eventLanes[eventName] = { values: inputValues, outputRanges, keys: keysArr, persistent: isPersistent };
479
+ keysArr.forEach(k => activeKeys.add(k));
480
+ }
481
+ }
482
+ const sortedX = Array.from(uniqueX).sort((a, b) => a - b);
483
+ const sortedY = Array.from(uniqueY).sort((a, b) => a - b);
484
+ const numericKeys = ['worldX', 'worldY', 'opacity'];
485
+ const colorKeys = [];
486
+ Array.from(activeKeys).forEach(k => {
487
+ if (k === 'worldX' || k === 'worldY' || k === 'opacity')
488
+ return;
489
+ if (isColorProp(k))
490
+ colorKeys.push(k);
491
+ else
492
+ numericKeys.push(k);
493
+ });
494
+ const finalize = (tAxis, fAxis, isH, keys, isColor) => {
495
+ return fAxis.map(fVal => {
496
+ const values = [];
497
+ const constFlags = [];
498
+ keys.forEach(k => {
499
+ const outputs = [];
500
+ let isConst = true;
501
+ const anchors = tAxis.filter(t => grid[isH ? `${t}_${fVal}` : `${fVal}_${t}`]).map(t => ({ t, v: grid[isH ? `${t}_${fVal}` : `${fVal}_${t}`][k] ?? homeProps[k] }));
502
+ tAxis.forEach((t, i) => {
503
+ let calc = homeProps[k];
504
+ if (anchors.length > 0) {
505
+ let l = anchors[0], r = anchors[anchors.length - 1];
506
+ for (let j = 0; j < anchors.length - 1; j++)
507
+ if (t >= anchors[j].t && t <= anchors[j + 1].t) {
508
+ l = anchors[j];
509
+ r = anchors[j + 1];
510
+ break;
511
+ }
512
+ if (t <= l.t)
513
+ calc = l.v;
514
+ else if (t >= r.t)
515
+ calc = r.v;
516
+ else {
517
+ if (isColor)
518
+ calc = jsInterpolateColor(t, l.t, r.t, l.v, r.v);
519
+ else
520
+ calc = l.v + ((t - l.t) / ((r.t - l.t) || 1)) * (r.v - l.v);
521
+ }
522
+ }
523
+ outputs.push(calc);
524
+ if (i > 0 && calc !== outputs[0])
525
+ isConst = false;
526
+ });
527
+ values.push(outputs);
528
+ constFlags.push(isConst);
529
+ });
530
+ return { fixed: fVal, values, constFlags };
531
+ });
532
+ };
533
+ if (pageId === 'ROOM' && frames) {
534
+ // Keep minimal or no logging here
535
+ }
536
+ return {
537
+ homeX: homeOffset.x, homeY: homeOffset.y, localX: localLayout.x, localY: localLayout.y,
538
+ bakedH: finalize(sortedX, sortedY, true, numericKeys, false),
539
+ bakedV: finalize(sortedY, sortedX, false, numericKeys, false),
540
+ bakedCH: finalize(sortedX, sortedY, true, colorKeys, true),
541
+ bakedCV: finalize(sortedY, sortedX, false, colorKeys, true),
542
+ eventLanes,
543
+ uniqueX: sortedX, uniqueY: sortedY,
544
+ numericKeys, colorKeys,
545
+ baseProps: stripLayoutProps(baseStyle),
546
+ homeProps
547
+ };
548
+ }
549
+ catch (e) {
550
+ console.error('[Aniview] Bake Failed', e);
551
+ return null;
552
+ }
553
+ }, [localLayout, config, pageId, frames, style, baseStyle, dimensions.offsetX, dimensions.offsetY, dimensions.width, dimensions.height]);
554
+ const cameraX_SV = events?.x;
555
+ const cameraY_SV = events?.y;
556
+ const isSingleRowVal = config.layout?.length === 1;
557
+ const animatedStyle = useAnimatedStyle(() => {
558
+ if (!baked || !cameraX_SV || !cameraY_SV || !config)
559
+ return { opacity: 0 };
560
+ const cameraX = cameraX_SV.value, cameraY = cameraY_SV.value;
561
+ // NATIVE VIRTUALIZATION CHECK (Spatial)
562
+ // We hide components that are more than 1.5 screen-widths away from the camera.
563
+ // This is 100% UI-thread safe and replaces the problematic JS Set check.
564
+ const thresholdX = dimensions.width * 1.5;
565
+ const thresholdY = dimensions.height * 1.5;
566
+ const distX = Math.abs(cameraX - baked.homeX);
567
+ const distY = Math.abs(cameraY - baked.homeY);
568
+ let isVisibleX = distX <= thresholdX;
569
+ if (!isVisibleX) {
570
+ for (let i = 0; i < baked.uniqueX.length; i++) {
571
+ if (Math.abs(cameraX - baked.uniqueX[i]) < thresholdX) {
572
+ isVisibleX = true;
573
+ break;
574
+ }
575
+ }
576
+ }
577
+ let isVisibleY = distY <= thresholdY;
578
+ if (!isVisibleY) {
579
+ for (let i = 0; i < baked.uniqueY.length; i++) {
580
+ if (Math.abs(cameraY - baked.uniqueY[i]) < thresholdY) {
581
+ isVisibleY = true;
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ if (!isVisibleX || !isVisibleY)
587
+ return { opacity: 0 };
588
+ // Safety: If dimensions are 0 (startup), skip calculation
589
+ if (dimensions.width <= 0 || dimensions.height <= 0 || isNaN(dimensions.width) || isNaN(dimensions.height)) {
590
+ return { opacity: 0 };
591
+ }
592
+ const windowX = dimensions.width * 0.4;
593
+ const windowY = dimensions.height * 0.4;
594
+ let pX = 1 - Math.min(1, Math.max(0, Math.abs(cameraX - baked.homeX) / windowX));
595
+ let pY = isSingleRowVal ? 1 : (1 - Math.min(1, Math.max(0, Math.abs(cameraY - baked.homeY) / windowY)));
596
+ // Sharpen the curve so events are exclusive and don't 'leak' visual offsets.
597
+ if (!pX || isNaN(pX))
598
+ pX = 0;
599
+ if (!pY || isNaN(pY))
600
+ pY = 0;
601
+ // Softened power-2 curve for smoother transitions and better responsiveness
602
+ const presenceX = Math.pow(pX, 2);
603
+ const presenceY = isSingleRowVal ? 1 : Math.pow(pY, 2);
604
+ const presence = presenceX * presenceY;
605
+ const getLaneInfo = (lanes, pos) => {
606
+ if (!lanes || lanes.length === 0)
607
+ return null;
608
+ let i = 0;
609
+ while (i < lanes.length - 1 && pos > lanes[i].fixed)
610
+ i++;
611
+ const l1idx = Math.max(0, i - 1), l2idx = i;
612
+ const l1 = lanes[l1idx], l2 = lanes[l2idx];
613
+ if (!l1 || !l2)
614
+ return null;
615
+ const gap = l2.fixed - l1.fixed;
616
+ const mix = Math.max(0, Math.min(1, gap > 0.1 ? (pos - l1.fixed) / gap : 0));
617
+ return { l1: { ...l1, idx: l1idx }, l2: { ...l2, idx: l2idx }, mix, dist: Math.min(Math.abs(pos - l1.fixed), Math.abs(pos - l2.fixed)) };
618
+ };
619
+ const laneH = getLaneInfo(baked.bakedH, cameraY), laneV = getLaneInfo(baked.bakedV, cameraX);
620
+ if (!laneH || !laneV)
621
+ return { opacity: 0 };
622
+ const viewportSize = dimensions.width;
623
+ const isNearH = laneH.dist < viewportSize, isNearV = laneV.dist < viewportSize;
624
+ if (!isNearH && !isNearV)
625
+ return { opacity: 0 };
626
+ const props = {
627
+ ...baked.baseProps.rest,
628
+ position: 'absolute',
629
+ width: baked.baseProps.rest.width ?? dimensions.width,
630
+ height: baked.baseProps.rest.height ?? dimensions.height,
631
+ };
632
+ // If user provided left/top in style, we should probably ignore them in the final composite
633
+ // because they are already in localLayout and baked.localX/Y.
634
+ props.left = 0;
635
+ props.top = 0;
636
+ const hWeight = isNearV ? (isNearH ? laneV.dist / (laneH.dist + laneV.dist + 0.001) : 0) : 1;
637
+ // ONE-TIME OPTIMIZATION: Search for segment indices ONCE per frame
638
+ const segX = getSegmentInfo(cameraX, baked.uniqueX);
639
+ const segY = getSegmentInfo(cameraY, baked.uniqueY);
640
+ let finalX = 0, finalY = 0, finalOp = 1;
641
+ const spatialVals = { ...baked.homeProps };
642
+ const runSpatial = (keys, baked_H, baked_V, isColor) => {
643
+ const laneH_L = baked_H[laneH.l1.idx || 0], laneH_R = baked_H[laneH.l2.idx || 0];
644
+ const laneV_L = baked_V[laneV.l1.idx || 0], laneV_R = baked_V[laneV.l2.idx || 0];
645
+ keys.forEach((k, i) => {
646
+ let vH = baked.homeProps[k], vV = baked.homeProps[k];
647
+ if (isNearH && laneH_L && laneH_R) {
648
+ const o1 = laneH_L.values[i], o2 = laneH_R.values[i];
649
+ if (o1 && o2) {
650
+ const r1 = (laneH_L.constFlags[i] || segX.constant) ? o1[segX.i] : (isColor ? smartInterpolateColor(cameraX, baked.uniqueX, o1) : o1[segX.i] + segX.p * (o1[segX.i + 1] - o1[segX.i]));
651
+ const r2 = (laneH_R.constFlags[i] || segX.constant) ? o2[segX.i] : (isColor ? smartInterpolateColor(cameraX, baked.uniqueX, o2) : o2[segX.i] + segX.p * (o2[segX.i + 1] - o2[segX.i]));
652
+ vH = isColor ? smartInterpolateColor(laneH.mix, [0, 1], [r1, r2]) : r1 + laneH.mix * (r2 - r1);
653
+ }
654
+ }
655
+ if (isNearV && laneV_L && laneV_R) {
656
+ const o1 = laneV_L.values[i], o2 = laneV_R.values[i];
657
+ if (o1 && o2) {
658
+ const r1 = (laneV_L.constFlags[i] || segY.constant) ? o1[segY.i] : (isColor ? smartInterpolateColor(cameraY, baked.uniqueY, o1) : o1[segY.i] + segY.p * (o1[segY.i + 1] - o1[segY.i]));
659
+ const r2 = (laneV_R.constFlags[i] || segY.constant) ? o2[segY.i] : (isColor ? smartInterpolateColor(cameraY, baked.uniqueY, o2) : o2[segY.i] + segY.p * (o2[segY.i + 1] - o2[segY.i]));
660
+ vV = isColor ? smartInterpolateColor(laneV.mix, [0, 1], [r1, r2]) : r1 + laneV.mix * (r2 - r1);
661
+ }
662
+ }
663
+ if (!isColor) {
664
+ if (typeof vH !== 'number' || isNaN(vH))
665
+ vH = baked.homeProps[k] || 0;
666
+ if (typeof vV !== 'number' || isNaN(vV))
667
+ vV = baked.homeProps[k] || 0;
668
+ }
669
+ spatialVals[k] = (isNearH && isNearV) ? (isColor ? smartInterpolateColor(hWeight, [0, 1], [vV, vH]) : vH * hWeight + vV * (1 - hWeight)) : (isNearH ? vH : vV);
670
+ });
671
+ };
672
+ runSpatial(baked.numericKeys, baked.bakedH, baked.bakedV, false);
673
+ if (baked.colorKeys.length > 0)
674
+ runSpatial(baked.colorKeys, baked.bakedCH, baked.bakedCV, true);
675
+ // 2. Event Phase
676
+ const eventResults = {};
677
+ const eventColorResults = {};
678
+ Object.entries(baked.eventLanes).forEach(([eName, lane]) => {
679
+ const driver = events[eName];
680
+ if (!driver)
681
+ return;
682
+ const val = driver.value;
683
+ const laneLen = lane.values.length;
684
+ const laneWeight = lane.persistent ? 1 : presence; // presence is ~1 on home page
685
+ // Debug Log
686
+ if (eName === 'pullDown') {
687
+ // console.log(`[Aniview Worklet] pullDown val: ${val} (Presence: ${presence.toFixed(2)})`);
688
+ }
689
+ lane.keys.forEach((k, i) => {
690
+ const isColor = baked.colorKeys.includes(k);
691
+ if (isColor) {
692
+ const range = lane.outputRanges[i];
693
+ let result;
694
+ // Safety: interpolateColor needs at least 2 points
695
+ if (laneLen < 2) {
696
+ result = range[0];
697
+ }
698
+ else {
699
+ result = interpolateColor(val, lane.values, range);
700
+ }
701
+ // Modulate color towards homeProps[k]
702
+ if (laneWeight < 0.99) {
703
+ eventColorResults[k] = smartInterpolateColor(laneWeight, [0, 1], [baked.homeProps[k], result]);
704
+ }
705
+ else {
706
+ eventColorResults[k] = result;
707
+ }
708
+ }
709
+ else {
710
+ const range = lane.outputRanges[i];
711
+ let result = baked.homeProps[k] ?? 0;
712
+ if (laneLen === 1) {
713
+ const v1 = lane.values[0];
714
+ if (Math.abs(v1) > 0.001) {
715
+ result = interpolate(val, [0, v1], [baked.homeProps[k] ?? 0, range[0]], Extrapolation.CLAMP);
716
+ }
717
+ else {
718
+ result = val > 0 ? range[0] : (baked.homeProps[k] ?? 0);
719
+ }
720
+ }
721
+ else if (laneLen >= 2) {
722
+ let hasDuplicate = false;
723
+ for (let j = 0; j < laneLen - 1; j++)
724
+ if (Math.abs(lane.values[j + 1] - lane.values[j]) < 0.0001)
725
+ hasDuplicate = true;
726
+ if (!hasDuplicate) {
727
+ result = interpolate(val, lane.values, range, Extrapolation.CLAMP);
728
+ }
729
+ else {
730
+ result = range[0];
731
+ }
732
+ }
733
+ const diff = (result - (baked.homeProps[k] ?? 0)) * laneWeight;
734
+ if (!eventResults[k])
735
+ eventResults[k] = (baked.homeProps[k] ?? 0) + diff;
736
+ else
737
+ eventResults[k] += diff;
738
+ }
739
+ });
740
+ });
741
+ // 3. Composition Phase
742
+ const compositeNumeric = (k, base) => {
743
+ const evVal = eventResults[k];
744
+ if (evVal === undefined)
745
+ return base ?? 0;
746
+ const homeVal = baked.homeProps[k] ?? 0;
747
+ const evDiff = evVal - homeVal;
748
+ if (k === 'opacity' || k === '_tr_scale') {
749
+ return (base ?? 0) * (evVal / (homeVal || 1));
750
+ }
751
+ return (base ?? 0) + evDiff;
752
+ };
753
+ finalX = compositeNumeric('worldX', spatialVals.worldX ?? 0);
754
+ finalY = compositeNumeric('worldY', spatialVals.worldY ?? 0);
755
+ finalOp = Math.max(0, Math.min(1, compositeNumeric('opacity', spatialVals.opacity ?? 1)));
756
+ const layoutXKeys = ['left', 'marginLeft'];
757
+ const layoutYKeys = ['top', 'marginTop'];
758
+ // Build final props
759
+ const allKeys = new Set([...baked.numericKeys, ...baked.colorKeys]);
760
+ allKeys.forEach(k => {
761
+ if (k === 'worldX' || k === 'worldY' || k === 'opacity')
762
+ return;
763
+ const spatialVal = spatialVals[k];
764
+ const isColor = baked.colorKeys.includes(k);
765
+ let res;
766
+ if (isColor) {
767
+ res = eventColorResults[k] ?? spatialVal ?? baked.homeProps[k];
768
+ }
769
+ else {
770
+ res = compositeNumeric(k, spatialVal);
771
+ }
772
+ if (!isColor && isNaN(res))
773
+ res = baked.homeProps[k];
774
+ // Redirect layout props to final transform to avoid double-dipping results.
775
+ // This ensures that animating 'marginTop' actually moves the component via translateY.
776
+ if (!isColor) {
777
+ if (layoutXKeys.includes(k)) {
778
+ finalX += (res - (baked.homeProps[k] ?? 0));
779
+ return;
780
+ }
781
+ if (layoutYKeys.includes(k)) {
782
+ finalY += (res - (baked.homeProps[k] ?? 0));
783
+ return;
784
+ }
785
+ }
786
+ if (k.startsWith('_tr_')) {
787
+ if (!props.transform)
788
+ props.transform = [];
789
+ const tName = k.slice(4);
790
+ let tVal = res;
791
+ if (tName === 'rotate' || tName === 'rotateX' || tName === 'rotateY' || tName === 'rotateZ')
792
+ tVal = `${res}deg`;
793
+ props.transform.push({ [tName]: tVal });
794
+ }
795
+ else if (k === 'shadowOffsetWidth' ||
796
+ k === 'shadowOffsetHeight' ||
797
+ k === 'textShadowOffsetWidth' ||
798
+ k === 'textShadowOffsetHeight') {
799
+ const b = k.startsWith('shadowOffset') ? 'shadowOffset' : 'textShadowOffset';
800
+ props[b] = { ...props[b], [k.endsWith('Width') ? 'width' : 'height']: res };
801
+ }
802
+ else
803
+ props[k] = res;
804
+ });
805
+ props.opacity = finalOp;
806
+ const sx = cameraX || 0, sy = cameraY || 0;
807
+ const tx = (finalX - sx), ty = (finalY - sy);
808
+ const baseTransform = baked.baseProps.transform || [];
809
+ props.transform = [{ translateX: isNaN(tx) ? 0 : tx }, { translateY: isNaN(ty) ? 0 : ty }, ...(props.transform || []), ...baseTransform];
810
+ return props;
811
+ }, [baked, dimensions.width, dimensions.height, cameraX_SV, cameraY_SV, events]);
812
+ // Final safety: If no layout yet, show placeholder at opacity 0
813
+ if (!localLayout || hasNoDims) {
814
+ return (<Animated.View onLayout={onLayout} style={[
815
+ baseStyle,
816
+ {
817
+ opacity: 0,
818
+ width: baseStyle.width ?? dimensions.width,
819
+ height: baseStyle.height ?? dimensions.height
820
+ }
821
+ ]} pointerEvents="none">
822
+ {children}
823
+ </Animated.View>);
824
+ }
825
+ if (!shouldRender && !persistent)
826
+ return null;
827
+ return (<Animated.View style={animatedStyle} {...rest}>
828
+ {children}
829
+ </Animated.View>);
830
+ }
831
+ //# sourceMappingURL=Aniview.js.map