aniview 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/Aniview.d.ts +63 -0
- package/dist/Aniview.d.ts.map +1 -0
- package/dist/Aniview.js +831 -0
- package/dist/Aniview.js.map +1 -0
- package/dist/AniviewPanel.d.ts +33 -0
- package/dist/AniviewPanel.d.ts.map +1 -0
- package/dist/AniviewPanel.js +66 -0
- package/dist/AniviewPanel.js.map +1 -0
- package/dist/GestureStressTest.d.ts +3 -0
- package/dist/GestureStressTest.d.ts.map +1 -0
- package/dist/GestureStressTest.js +125 -0
- package/dist/GestureStressTest.js.map +1 -0
- package/dist/aniviewConfig.d.ts +175 -0
- package/dist/aniviewConfig.d.ts.map +1 -0
- package/dist/aniviewConfig.js +568 -0
- package/dist/aniviewConfig.js.map +1 -0
- package/dist/aniviewProvider.d.ts +93 -0
- package/dist/aniviewProvider.d.ts.map +1 -0
- package/dist/aniviewProvider.js +229 -0
- package/dist/aniviewProvider.js.map +1 -0
- package/dist/core/AniviewLock.d.ts +16 -0
- package/dist/core/AniviewLock.d.ts.map +1 -0
- package/dist/core/AniviewLock.js +18 -0
- package/dist/core/AniviewLock.js.map +1 -0
- package/dist/core/AniviewMath.d.ts +41 -0
- package/dist/core/AniviewMath.d.ts.map +1 -0
- package/dist/core/AniviewMath.js +69 -0
- package/dist/core/AniviewMath.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/useAniview.d.ts +39 -0
- package/dist/useAniview.d.ts.map +1 -0
- package/dist/useAniview.js +32 -0
- package/dist/useAniview.js.map +1 -0
- package/dist/useAniviewContext.d.ts +156 -0
- package/dist/useAniviewContext.d.ts.map +1 -0
- package/dist/useAniviewContext.js +3 -0
- package/dist/useAniviewContext.js.map +1 -0
- package/dist/useAniviewLock.d.ts +20 -0
- package/dist/useAniviewLock.d.ts.map +1 -0
- package/dist/useAniviewLock.js +32 -0
- package/dist/useAniviewLock.js.map +1 -0
- package/package.json +60 -0
- package/src/Aniview.tsx +868 -0
- package/src/AniviewPanel.tsx +141 -0
- package/src/GestureStressTest.tsx +144 -0
- package/src/__tests__/AniviewLock.test.ts +58 -0
- package/src/__tests__/AniviewMath.test.ts +211 -0
- package/src/__tests__/AniviewSnapshot.test.tsx +85 -0
- package/src/__tests__/__snapshots__/AniviewSnapshot.test.tsx.snap +7 -0
- package/src/__tests__/aniviewConfig.test.ts +70 -0
- package/src/aniviewConfig.tsx +688 -0
- package/src/aniviewProvider.tsx +307 -0
- package/src/core/AniviewLock.ts +23 -0
- package/src/core/AniviewMath.ts +107 -0
- package/src/index.ts +6 -0
- package/src/useAniview.tsx +75 -0
- package/src/useAniviewContext.tsx +170 -0
- package/src/useAniviewLock.tsx +37 -0
package/dist/Aniview.js
ADDED
|
@@ -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
|