aniview 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +40 -21
  2. package/README.md +93 -97
  3. package/dist/Aniview.d.ts +1 -0
  4. package/dist/Aniview.d.ts.map +1 -1
  5. package/dist/Aniview.js +24 -682
  6. package/dist/Aniview.js.map +1 -1
  7. package/dist/AniviewPanel.d.ts.map +1 -1
  8. package/dist/aniviewConfig.d.ts +98 -6
  9. package/dist/aniviewConfig.d.ts.map +1 -1
  10. package/dist/aniviewConfig.js +109 -338
  11. package/dist/aniviewConfig.js.map +1 -1
  12. package/dist/aniviewProvider.d.ts +3 -1
  13. package/dist/aniviewProvider.d.ts.map +1 -1
  14. package/dist/aniviewProvider.js +24 -5
  15. package/dist/aniviewProvider.js.map +1 -1
  16. package/dist/core/AniviewBake.d.ts +37 -0
  17. package/dist/core/AniviewBake.d.ts.map +1 -0
  18. package/dist/core/AniviewBake.js +309 -0
  19. package/dist/core/AniviewBake.js.map +1 -0
  20. package/dist/core/AniviewColor.d.ts +62 -0
  21. package/dist/core/AniviewColor.d.ts.map +1 -0
  22. package/dist/core/AniviewColor.js +199 -0
  23. package/dist/core/AniviewColor.js.map +1 -0
  24. package/dist/core/AniviewGesture.d.ts +46 -0
  25. package/dist/core/AniviewGesture.d.ts.map +1 -0
  26. package/dist/core/AniviewGesture.js +344 -0
  27. package/dist/core/AniviewGesture.js.map +1 -0
  28. package/dist/core/AniviewLock.d.ts +8 -0
  29. package/dist/core/AniviewLock.d.ts.map +1 -1
  30. package/dist/core/AniviewLock.js +8 -0
  31. package/dist/core/AniviewLock.js.map +1 -1
  32. package/dist/core/AniviewMath.d.ts +31 -7
  33. package/dist/core/AniviewMath.d.ts.map +1 -1
  34. package/dist/core/AniviewMath.js +31 -7
  35. package/dist/core/AniviewMath.js.map +1 -1
  36. package/dist/core/AniviewStyle.d.ts +34 -0
  37. package/dist/core/AniviewStyle.d.ts.map +1 -0
  38. package/dist/core/AniviewStyle.js +333 -0
  39. package/dist/core/AniviewStyle.js.map +1 -0
  40. package/dist/core/AniviewStyleUtils.d.ts +157 -0
  41. package/dist/core/AniviewStyleUtils.d.ts.map +1 -0
  42. package/dist/core/AniviewStyleUtils.js +44 -0
  43. package/dist/core/AniviewStyleUtils.js.map +1 -0
  44. package/dist/useAniview.js +2 -0
  45. package/dist/useAniview.js.map +1 -1
  46. package/dist/useAniviewContext.d.ts +75 -1
  47. package/dist/useAniviewContext.d.ts.map +1 -1
  48. package/dist/useAniviewContext.js.map +1 -1
  49. package/dist/useAniviewLock.d.ts +2 -5
  50. package/dist/useAniviewLock.d.ts.map +1 -1
  51. package/dist/useAniviewLock.js +13 -5
  52. package/dist/useAniviewLock.js.map +1 -1
  53. package/package.json +7 -6
  54. package/src/Aniview.tsx +43 -704
  55. package/src/__tests__/AniviewBake.test.ts +282 -0
  56. package/src/__tests__/AniviewColor.test.ts +431 -0
  57. package/src/__tests__/AniviewSnapshot.test.tsx +30 -33
  58. package/src/__tests__/__snapshots__/AniviewSnapshot.test.tsx.snap +183 -3
  59. package/src/aniviewConfig.tsx +115 -363
  60. package/src/aniviewProvider.tsx +29 -9
  61. package/src/core/AniviewBake.ts +310 -0
  62. package/src/core/AniviewColor.ts +219 -0
  63. package/src/core/AniviewGesture.ts +433 -0
  64. package/src/core/AniviewLock.ts +31 -23
  65. package/src/core/AniviewMath.ts +31 -7
  66. package/src/core/AniviewStyle.ts +400 -0
  67. package/src/core/AniviewStyleUtils.ts +49 -0
  68. package/src/useAniview.tsx +2 -0
  69. package/src/useAniviewContext.tsx +75 -3
  70. package/src/useAniviewLock.tsx +14 -6
package/dist/Aniview.js CHANGED
@@ -1,172 +1,9 @@
1
1
  import React, { useState, useCallback, useMemo, useEffect } from 'react';
2
2
  import { StyleSheet } from 'react-native';
3
- import Animated, { useAnimatedStyle, interpolate, interpolateColor, Extrapolation, useAnimatedReaction, runOnJS } from 'react-native-reanimated';
3
+ import Animated, { useAnimatedStyle, useAnimatedReaction, runOnJS } from 'react-native-reanimated';
4
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
- }
5
+ import { bakeKeyframes } from './core/AniviewBake';
6
+ import { computeAnimatedStyle } from './core/AniviewStyle';
170
7
  /**
171
8
  * **Aniview** — Absolute World Coordinate Animation Engine Component
172
9
  *
@@ -207,6 +44,7 @@ function getSegmentInfo(val, input) {
207
44
  * @param props.persistent - If `true`, stays mounted even when far offscreen. Required for WebGL/Three.js canvases. Default: `false`
208
45
  * @param props.style - Base styles applied when at home position. Numeric and color props here become the interpolation baseline.
209
46
  * @param props.pointerEvents - Standard RN pointer events. Useful for overlay pages that shouldn't block touches.
47
+ * @returns Animated view that interpolates styles from spatial and event lanes.
210
48
  *
211
49
  * @example
212
50
  * ```tsx
@@ -232,19 +70,24 @@ export default function Aniview(props) {
232
70
  const { events, config, dimensions, isMoving } = context;
233
71
  const persistent = props.persistent ?? false;
234
72
  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;
73
+ // Pre-calculate home page offset on the JS thread to avoid UI thread class calls.
74
+ const homeOffset = useMemo(() => {
75
+ if (!config || !dimensions.width || !dimensions.height)
76
+ return { x: 0, y: 0 };
239
77
  const homeId = config.resolvePageId(pageId);
240
- return config.getPageOffset(homeId, dimensions).x;
78
+ return config.getPageOffset(homeId, dimensions);
241
79
  }, [config, pageId, dimensions.width, dimensions.height]);
80
+ const homeX = homeOffset.x;
81
+ const homeY = homeOffset.y;
242
82
  // SELECTIVE UNMOUNTING: If not persistent, monitor visibility natively.
243
83
  useAnimatedReaction(() => {
244
84
  if (persistent || !isMoving || isMoving.value)
245
85
  return null;
246
86
  const cameraX = events.x.value;
247
- const isFar = Math.abs(cameraX - homeX) > dimensions.width * 1.5;
87
+ const cameraY = events.y.value;
88
+ const isFarX = Math.abs(cameraX - homeX) > dimensions.width * 1.5;
89
+ const isFarY = Math.abs(cameraY - homeY) > dimensions.height * 1.5;
90
+ const isFar = isFarX || isFarY;
248
91
  return isFar;
249
92
  }, (isFar) => {
250
93
  if (isFar === null)
@@ -253,7 +96,7 @@ export default function Aniview(props) {
253
96
  runOnJS(setShouldRender)(false);
254
97
  if (!isFar && !shouldRender)
255
98
  runOnJS(setShouldRender)(true);
256
- }, [persistent, dimensions.width, homeX, shouldRender, isMoving]);
99
+ }, [persistent, dimensions.width, dimensions.height, homeX, homeY, shouldRender, isMoving]);
257
100
  const hasNoDims = dimensions.width <= 0 || dimensions.height <= 0;
258
101
  const cached = useMemo(() => config?.getLayout(pageId.toString()), [config, pageId]);
259
102
  const [localLayout, setLocalLayout] = useState(cached || null);
@@ -271,6 +114,12 @@ export default function Aniview(props) {
271
114
  setLocalLayout({ x, y });
272
115
  }
273
116
  }, [style, localLayout]);
117
+ /**
118
+ * Captures local layout origin for world composition and cache hydration.
119
+ *
120
+ * @param e - Native layout event from the root animated view.
121
+ * @returns void
122
+ */
274
123
  const onLayout = useCallback((e) => {
275
124
  let { x, y, width, height } = e.nativeEvent.layout;
276
125
  const flattened = StyleSheet.flatten(props.style || {});
@@ -292,264 +141,7 @@ export default function Aniview(props) {
292
141
  const baked = useMemo(() => {
293
142
  if (!localLayout || !config)
294
143
  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
- }
144
+ return bakeKeyframes(localLayout, config, pageId, dimensions, frames, baseStyle, style);
553
145
  }, [localLayout, config, pageId, frames, style, baseStyle, dimensions.offsetX, dimensions.offsetY, dimensions.width, dimensions.height]);
554
146
  const cameraX_SV = events?.x;
555
147
  const cameraY_SV = events?.y;
@@ -557,257 +149,7 @@ export default function Aniview(props) {
557
149
  const animatedStyle = useAnimatedStyle(() => {
558
150
  if (!baked || !cameraX_SV || !cameraY_SV || !config)
559
151
  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;
152
+ return computeAnimatedStyle(baked, cameraX_SV, cameraY_SV, events, config, dimensions, isSingleRowVal);
811
153
  }, [baked, dimensions.width, dimensions.height, cameraX_SV, cameraY_SV, events]);
812
154
  // Final safety: If no layout yet, show placeholder at opacity 0
813
155
  if (!localLayout || hasNoDims) {