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.
- package/CHANGELOG.md +40 -21
- package/README.md +93 -97
- package/dist/Aniview.d.ts +1 -0
- package/dist/Aniview.d.ts.map +1 -1
- package/dist/Aniview.js +24 -682
- package/dist/Aniview.js.map +1 -1
- package/dist/AniviewPanel.d.ts.map +1 -1
- package/dist/aniviewConfig.d.ts +98 -6
- package/dist/aniviewConfig.d.ts.map +1 -1
- package/dist/aniviewConfig.js +109 -338
- package/dist/aniviewConfig.js.map +1 -1
- package/dist/aniviewProvider.d.ts +3 -1
- package/dist/aniviewProvider.d.ts.map +1 -1
- package/dist/aniviewProvider.js +24 -5
- package/dist/aniviewProvider.js.map +1 -1
- package/dist/core/AniviewBake.d.ts +37 -0
- package/dist/core/AniviewBake.d.ts.map +1 -0
- package/dist/core/AniviewBake.js +309 -0
- package/dist/core/AniviewBake.js.map +1 -0
- package/dist/core/AniviewColor.d.ts +62 -0
- package/dist/core/AniviewColor.d.ts.map +1 -0
- package/dist/core/AniviewColor.js +199 -0
- package/dist/core/AniviewColor.js.map +1 -0
- package/dist/core/AniviewGesture.d.ts +46 -0
- package/dist/core/AniviewGesture.d.ts.map +1 -0
- package/dist/core/AniviewGesture.js +344 -0
- package/dist/core/AniviewGesture.js.map +1 -0
- package/dist/core/AniviewLock.d.ts +8 -0
- package/dist/core/AniviewLock.d.ts.map +1 -1
- package/dist/core/AniviewLock.js +8 -0
- package/dist/core/AniviewLock.js.map +1 -1
- package/dist/core/AniviewMath.d.ts +31 -7
- package/dist/core/AniviewMath.d.ts.map +1 -1
- package/dist/core/AniviewMath.js +31 -7
- package/dist/core/AniviewMath.js.map +1 -1
- package/dist/core/AniviewStyle.d.ts +34 -0
- package/dist/core/AniviewStyle.d.ts.map +1 -0
- package/dist/core/AniviewStyle.js +333 -0
- package/dist/core/AniviewStyle.js.map +1 -0
- package/dist/core/AniviewStyleUtils.d.ts +157 -0
- package/dist/core/AniviewStyleUtils.d.ts.map +1 -0
- package/dist/core/AniviewStyleUtils.js +44 -0
- package/dist/core/AniviewStyleUtils.js.map +1 -0
- package/dist/useAniview.js +2 -0
- package/dist/useAniview.js.map +1 -1
- package/dist/useAniviewContext.d.ts +75 -1
- package/dist/useAniviewContext.d.ts.map +1 -1
- package/dist/useAniviewContext.js.map +1 -1
- package/dist/useAniviewLock.d.ts +2 -5
- package/dist/useAniviewLock.d.ts.map +1 -1
- package/dist/useAniviewLock.js +13 -5
- package/dist/useAniviewLock.js.map +1 -1
- package/package.json +7 -6
- package/src/Aniview.tsx +43 -704
- package/src/__tests__/AniviewBake.test.ts +282 -0
- package/src/__tests__/AniviewColor.test.ts +431 -0
- package/src/__tests__/AniviewSnapshot.test.tsx +30 -33
- package/src/__tests__/__snapshots__/AniviewSnapshot.test.tsx.snap +183 -3
- package/src/aniviewConfig.tsx +115 -363
- package/src/aniviewProvider.tsx +29 -9
- package/src/core/AniviewBake.ts +310 -0
- package/src/core/AniviewColor.ts +219 -0
- package/src/core/AniviewGesture.ts +433 -0
- package/src/core/AniviewLock.ts +31 -23
- package/src/core/AniviewMath.ts +31 -7
- package/src/core/AniviewStyle.ts +400 -0
- package/src/core/AniviewStyleUtils.ts +49 -0
- package/src/useAniview.tsx +2 -0
- package/src/useAniviewContext.tsx +75 -3
- 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,
|
|
3
|
+
import Animated, { useAnimatedStyle, useAnimatedReaction, runOnJS } from 'react-native-reanimated';
|
|
4
4
|
import { useAniview } from './useAniview';
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
236
|
-
const
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|