@symbiote-native/engine 0.1.7 → 0.2.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/build/alert/index.ios.js +10 -0
- package/build/animated/color.d.ts +2 -8
- package/build/animated/color.js +4 -45
- package/build/animated/easing.js +0 -8
- package/build/animated/graph.d.ts +13 -5
- package/build/animated/graph.js +85 -11
- package/build/animated/index.d.ts +2 -2
- package/build/animated/index.js +2 -2
- package/build/animated/interpolation.js +1 -1
- package/build/animated/leaf-lifecycle.d.ts +7 -0
- package/build/animated/leaf-lifecycle.js +185 -0
- package/build/animated/native/native-animated.js +1 -0
- package/build/animated/rgba.d.ts +8 -0
- package/build/animated/rgba.js +55 -0
- package/build/animated/value.js +12 -7
- package/build/commit.js +1 -5
- package/build/debug.d.ts +1 -1
- package/build/debug.js +10 -3
- package/build/events/index.js +0 -2
- package/build/index.d.ts +4 -2
- package/build/index.js +2 -1
- package/build/layout-animation/index.js +0 -1
- package/build/process-font-variant.js +0 -1
- package/build/process-transform/index.js +13 -19
- package/build/report-error.d.ts +5 -0
- package/build/report-error.js +60 -0
- package/build/status-bar/index.android.js +5 -6
- package/build/style-registry/index.js +95 -16
- package/build/style-sheet/index.js +4 -2
- package/build/surface.js +2 -1
- package/build/vibration/index.android.js +2 -3
- package/package.json +1 -1
- package/build/animated/interpolation-node.d.ts +0 -15
- package/build/animated/interpolation-node.js +0 -58
- package/build/style-registry/scope/index.d.ts +0 -3
- package/build/style-registry/scope/index.js +0 -41
- package/build/touch-history/index.d.ts +0 -23
- package/build/touch-history/index.js +0 -150
- package/build/type-guards/index.d.ts +0 -4
- package/build/type-guards/index.js +0 -15
package/build/alert/index.ios.js
CHANGED
|
@@ -43,6 +43,16 @@ function prompt(title, message, callbackOrButtons, type = 'plain-text', defaultV
|
|
|
43
43
|
if (btn.isPreferred) {
|
|
44
44
|
preferredButtonKey = String(index);
|
|
45
45
|
}
|
|
46
|
+
// UPSTREAM-BUG(react-native): Libraries/Alert/Alert.js:173 - a TRAILING button with no text
|
|
47
|
+
// is left out of the native buttons array while its onPress still occupies callbacks[index],
|
|
48
|
+
// so that handler can never fire: native has no id to return for a button it was never told
|
|
49
|
+
// about. Only the last button, only when textless, which reads more like leftover
|
|
50
|
+
// length - 1 bookkeeping than an intended "omit a blank trailing button" affordance.
|
|
51
|
+
// Ported for parity; do NOT fix without recording a deliberate divergence.
|
|
52
|
+
//
|
|
53
|
+
// The `!== undefined` here IS a deliberate divergence from upstream's truthy `btn.text`:
|
|
54
|
+
// an explicit `text: ''` is a caller asking for a blank label, not an omission, and this
|
|
55
|
+
// codebase treats falsy-vs-absent as distinct on principle. Upstream drops that button.
|
|
46
56
|
if (btn.text !== undefined || index < callbackOrButtons.length - 1) {
|
|
47
57
|
buttons.push({ [index]: btn.text ?? '' });
|
|
48
58
|
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { AnimatedWithChildren } from './graph';
|
|
2
2
|
import { AnimatedValue } from './value';
|
|
3
3
|
import type { INativeNodeConfig, IPlatformConfig } from './native/native-animated';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
g: number;
|
|
7
|
-
b: number;
|
|
8
|
-
a: number;
|
|
9
|
-
}
|
|
4
|
+
import { normalizeColor, type IRgbaValue } from './rgba';
|
|
5
|
+
export { normalizeColor, type IRgbaValue };
|
|
10
6
|
type IChannel = number | AnimatedValue;
|
|
11
7
|
interface IRgbaInput {
|
|
12
8
|
r: IChannel;
|
|
@@ -15,7 +11,6 @@ interface IRgbaInput {
|
|
|
15
11
|
a: IChannel;
|
|
16
12
|
}
|
|
17
13
|
export type IColorInput = IRgbaInput | IRgbaValue | string | number;
|
|
18
|
-
export declare function normalizeColor(color: string | number): IRgbaValue | undefined;
|
|
19
14
|
export declare class AnimatedColor extends AnimatedWithChildren {
|
|
20
15
|
readonly r: AnimatedValue;
|
|
21
16
|
readonly g: AnimatedValue;
|
|
@@ -34,4 +29,3 @@ export declare class AnimatedColor extends AnimatedWithChildren {
|
|
|
34
29
|
__makeNative(platformConfig?: IPlatformConfig): void;
|
|
35
30
|
__getNativeConfig(): INativeNodeConfig;
|
|
36
31
|
}
|
|
37
|
-
export {};
|
package/build/animated/color.js
CHANGED
|
@@ -6,57 +6,16 @@
|
|
|
6
6
|
// normalizeColor / processColorObject, which live outside shared).
|
|
7
7
|
import { AnimatedWithChildren, flushValue } from './graph.js';
|
|
8
8
|
import { AnimatedValue } from './value.js';
|
|
9
|
-
|
|
9
|
+
import { DEFAULT_COLOR, normalizeColor } from './rgba.js';
|
|
10
|
+
// Re-exported so every existing `from './color'` import keeps working; the definitions now
|
|
11
|
+
// live in the dependency-free ./rgba leaf (see its header for the cycle it breaks).
|
|
12
|
+
export { normalizeColor };
|
|
10
13
|
function isRgbaInput(value) {
|
|
11
14
|
return typeof value === 'object' && value !== null && 'r' in value && 'g' in value;
|
|
12
15
|
}
|
|
13
16
|
function toChannel(value) {
|
|
14
17
|
return value instanceof AnimatedValue ? value : new AnimatedValue(value);
|
|
15
18
|
}
|
|
16
|
-
// Decompose a #hex (3/4/6/8), rgb()/rgba(), or 0xRRGGBBAA number into channels.
|
|
17
|
-
// undefined when unparseable (a named/platform color), so the caller falls back to
|
|
18
|
-
// the default rather than throwing inside a render. Exported so interpolation's
|
|
19
|
-
// color path parses through the same RGBA decoder (DRY) rather than duplicating it.
|
|
20
|
-
export function normalizeColor(color) {
|
|
21
|
-
if (typeof color === 'number') {
|
|
22
|
-
const c = color >>> 0;
|
|
23
|
-
return { r: (c >>> 24) & 255, g: (c >>> 16) & 255, b: (c >>> 8) & 255, a: (c & 255) / 255 };
|
|
24
|
-
}
|
|
25
|
-
const trimmed = color.trim();
|
|
26
|
-
if (trimmed.startsWith('#'))
|
|
27
|
-
return parseHex(trimmed);
|
|
28
|
-
if (/^rgba?\(/i.test(trimmed))
|
|
29
|
-
return parseRgb(trimmed);
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
function parseHex(hex) {
|
|
33
|
-
let body = hex.slice(1);
|
|
34
|
-
if (body.length === 3 || body.length === 4) {
|
|
35
|
-
body = body
|
|
36
|
-
.split('')
|
|
37
|
-
.map(c => c + c)
|
|
38
|
-
.join('');
|
|
39
|
-
}
|
|
40
|
-
if (body.length !== 6 && body.length !== 8)
|
|
41
|
-
return undefined;
|
|
42
|
-
const int = Number.parseInt(body, 16);
|
|
43
|
-
if (Number.isNaN(int))
|
|
44
|
-
return undefined;
|
|
45
|
-
if (body.length === 6) {
|
|
46
|
-
return { r: (int >>> 16) & 255, g: (int >>> 8) & 255, b: int & 255, a: 1 };
|
|
47
|
-
}
|
|
48
|
-
const c = int >>> 0;
|
|
49
|
-
return { r: (c >>> 24) & 255, g: (c >>> 16) & 255, b: (c >>> 8) & 255, a: (c & 255) / 255 };
|
|
50
|
-
}
|
|
51
|
-
function parseRgb(str) {
|
|
52
|
-
const match = /^rgba?\(([^)]+)\)$/i.exec(str);
|
|
53
|
-
if (match === null)
|
|
54
|
-
return undefined;
|
|
55
|
-
const parts = match[1].split(',').map(part => Number.parseFloat(part.trim()));
|
|
56
|
-
if (parts.length < 3 || parts.some(n => Number.isNaN(n)))
|
|
57
|
-
return undefined;
|
|
58
|
-
return { r: parts[0], g: parts[1], b: parts[2], a: parts.length >= 4 ? parts[3] : 1 };
|
|
59
|
-
}
|
|
60
19
|
// Resolve any input form to four concrete channel values (numbers or pre-built
|
|
61
20
|
// AnimatedValues), so the constructor can wrap each in an AnimatedValue.
|
|
62
21
|
function resolveInput(value) {
|
package/build/animated/easing.js
CHANGED
|
@@ -4,15 +4,12 @@
|
|
|
4
4
|
import { bezier } from './bezier.js';
|
|
5
5
|
let cachedEase;
|
|
6
6
|
export const Easing = {
|
|
7
|
-
// A stepping function: 1 for any positive n, else 0.
|
|
8
7
|
step0(n) {
|
|
9
8
|
return n > 0 ? 1 : 0;
|
|
10
9
|
},
|
|
11
|
-
// A stepping function: 1 once n >= 1, else 0.
|
|
12
10
|
step1(n) {
|
|
13
11
|
return n >= 1 ? 1 : 0;
|
|
14
12
|
},
|
|
15
|
-
// Linear, f(t) = t.
|
|
16
13
|
linear(t) {
|
|
17
14
|
return t;
|
|
18
15
|
},
|
|
@@ -23,11 +20,9 @@ export const Easing = {
|
|
|
23
20
|
}
|
|
24
21
|
return cachedEase(t);
|
|
25
22
|
},
|
|
26
|
-
// Quadratic, f(t) = t * t.
|
|
27
23
|
quad(t) {
|
|
28
24
|
return t * t;
|
|
29
25
|
},
|
|
30
|
-
// Cubic, f(t) = t * t * t.
|
|
31
26
|
cubic(t) {
|
|
32
27
|
return t * t * t;
|
|
33
28
|
},
|
|
@@ -35,15 +30,12 @@ export const Easing = {
|
|
|
35
30
|
poly(n) {
|
|
36
31
|
return t => Math.pow(t, n);
|
|
37
32
|
},
|
|
38
|
-
// Sinusoidal.
|
|
39
33
|
sin(t) {
|
|
40
34
|
return 1 - Math.cos((t * Math.PI) / 2);
|
|
41
35
|
},
|
|
42
|
-
// Circular.
|
|
43
36
|
circle(t) {
|
|
44
37
|
return 1 - Math.sqrt(1 - t * t);
|
|
45
38
|
},
|
|
46
|
-
// Exponential.
|
|
47
39
|
exp(t) {
|
|
48
40
|
return Math.pow(2, 10 * (t - 1));
|
|
49
41
|
},
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { type INativeNodeConfig, type IPlatformConfig } from './native/native-animated';
|
|
2
|
-
import type
|
|
3
|
-
import type { AnimatedInterpolation } from './interpolation-node';
|
|
2
|
+
import { type IInterpolationConfig } from './interpolation';
|
|
4
3
|
export type IValueListener = (state: {
|
|
5
4
|
value: number | string;
|
|
6
5
|
}) => void;
|
|
7
|
-
type IInterpolationFactory = (parent: AnimatedNode, config: IInterpolationConfig) => AnimatedInterpolation;
|
|
8
|
-
export declare function registerInterpolationFactory(factory: IInterpolationFactory): void;
|
|
9
6
|
export declare class AnimatedNode {
|
|
10
7
|
private readonly listeners;
|
|
11
8
|
private suspendCallbacks;
|
|
@@ -41,4 +38,15 @@ export declare class AnimatedWithChildren extends AnimatedNode {
|
|
|
41
38
|
__callListeners(value: number | string): void;
|
|
42
39
|
}
|
|
43
40
|
export declare function flushValue(rootNode: AnimatedNode): void;
|
|
44
|
-
export {
|
|
41
|
+
export declare class AnimatedInterpolation extends AnimatedWithChildren {
|
|
42
|
+
private readonly parent;
|
|
43
|
+
private readonly config;
|
|
44
|
+
private interpolation;
|
|
45
|
+
constructor(parent: AnimatedNode, config: IInterpolationConfig);
|
|
46
|
+
private getInterpolation;
|
|
47
|
+
__getValue(): number | string;
|
|
48
|
+
__attach(): void;
|
|
49
|
+
__detach(): void;
|
|
50
|
+
__makeNative(platformConfig?: IPlatformConfig): void;
|
|
51
|
+
__getNativeConfig(): INativeNodeConfig;
|
|
52
|
+
}
|
package/build/animated/graph.js
CHANGED
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
// B) bottom-up: each flagged leaf re-pulls its whole subtree via __getValue()
|
|
11
11
|
// to rebuild composite props (e.g. transform from many parents).
|
|
12
12
|
import { generateNativeNodeTag, nativeAnimated, } from './native/native-animated.js';
|
|
13
|
+
import { checkValidRanges, createInterpolation } from './interpolation.js';
|
|
14
|
+
import { dlog } from '../debug.js';
|
|
13
15
|
let nextListenerId = 1;
|
|
14
|
-
let createInterpolationNode;
|
|
15
|
-
export function registerInterpolationFactory(factory) {
|
|
16
|
-
createInterpolationNode = factory;
|
|
17
|
-
}
|
|
18
16
|
// Depth of the currently-active withSuspendedCallbacks blocks. While > 0, a
|
|
19
17
|
// composite setter (AnimatedColor.setValue) is driving several channels in a row;
|
|
20
18
|
// each channel's own flushValue is suppressed so the bound leaf commits ONCE (at
|
|
@@ -104,13 +102,10 @@ export class AnimatedNode {
|
|
|
104
102
|
}
|
|
105
103
|
// Map this node's value before it reaches a prop, e.g. 0..1 -> 0..10. Defined once
|
|
106
104
|
// here (Information Expert: every node subclass wants the identical one-liner)
|
|
107
|
-
// instead of duplicated per subclass
|
|
108
|
-
//
|
|
105
|
+
// instead of duplicated per subclass. AnimatedInterpolation lives at the bottom of
|
|
106
|
+
// THIS file rather than its own module on purpose — see the comment there.
|
|
109
107
|
interpolate(config) {
|
|
110
|
-
|
|
111
|
-
throw new Error('AnimatedNode.interpolate: interpolation factory not registered');
|
|
112
|
-
}
|
|
113
|
-
return createInterpolationNode(this, config);
|
|
108
|
+
return new AnimatedInterpolation(this, config);
|
|
114
109
|
}
|
|
115
110
|
__addChild(_child) { }
|
|
116
111
|
__removeChild(_child) { }
|
|
@@ -199,8 +194,18 @@ export class AnimatedWithChildren extends AnimatedNode {
|
|
|
199
194
|
super.__callListeners(value);
|
|
200
195
|
// A native-driven node's children are updated natively; don't also walk them
|
|
201
196
|
// here (their values aren't tracked in JS while native owns the animation).
|
|
202
|
-
|
|
197
|
+
//
|
|
198
|
+
// Diagnostic seam, permanent: this early return is what silences a JS listener sitting on a
|
|
199
|
+
// CHILD of a value that was made native (a sticky header's interpolation under a natively
|
|
200
|
+
// attached scroll value). A consumer whose only driver is that listener then goes dead with no
|
|
201
|
+
// other symptom - see the sticky-header bootstrap notes. Logged only when children are
|
|
202
|
+
// actually being skipped, so a childless native value stays quiet.
|
|
203
|
+
if (this.isNative) {
|
|
204
|
+
if (this.children.length > 0) {
|
|
205
|
+
dlog(`STICKY[cascade] native value silences ${this.children.length} child JS listener(s) value=${String(value)}`);
|
|
206
|
+
}
|
|
203
207
|
return;
|
|
208
|
+
}
|
|
204
209
|
for (const child of this.children) {
|
|
205
210
|
child.__callListeners(numericValueOf(child));
|
|
206
211
|
}
|
|
@@ -238,3 +243,72 @@ export function flushValue(rootNode) {
|
|
|
238
243
|
collect(rootNode);
|
|
239
244
|
leaves.forEach(update => update());
|
|
240
245
|
}
|
|
246
|
+
// AnimatedInterpolation: a graph node that maps its parent's numeric value through an
|
|
247
|
+
// interpolation. Ported from RN's AnimatedInterpolation.js (numeric, string-with-units and
|
|
248
|
+
// color output ranges; platform colors stay out of scope, color.ts defers them).
|
|
249
|
+
//
|
|
250
|
+
// IT LIVES HERE, NOT IN ITS OWN MODULE, AND MUST STAY HERE. It used to sit in
|
|
251
|
+
// `interpolation-node.ts` and hand its constructor back to `AnimatedNode.interpolate()` through
|
|
252
|
+
// a `registerInterpolationFactory` call executed at module load — the usual trick for breaking
|
|
253
|
+
// an import cycle (this module owns the base classes that module extended). That trick is
|
|
254
|
+
// UNSOUND under Metro's production bundle: `inlineRequires` (production-only) defers a module's
|
|
255
|
+
// `require()` to the first use of its binding, nothing in the codebase ever names
|
|
256
|
+
// `AnimatedInterpolation` as a value, and a barrel's `export { X } from './x'` compiles to a
|
|
257
|
+
// lazy getter — so the module never evaluated, the factory was never registered, and the first
|
|
258
|
+
// `.interpolate()` on a device threw `interpolation factory not registered`, blanking the screen
|
|
259
|
+
// in RELEASE builds only (dev was fine, every headless test was fine, and the code was even
|
|
260
|
+
// present in the bundle — it just never ran). Adding a bare `import './interpolation-node'` to
|
|
261
|
+
// the barrel does NOT fix it either: Babel merges that import with the adjacent re-export of the
|
|
262
|
+
// same specifier, and the merged dependency stays lazy. Both were tried on device 2026-08-14.
|
|
263
|
+
//
|
|
264
|
+
// Keeping the class next to its base class removes the cycle, the factory, and the entire class
|
|
265
|
+
// of load-order bug with it: `interpolate()` constructs it directly and there is nothing left to
|
|
266
|
+
// register. Do not "tidy" it back into its own file.
|
|
267
|
+
export class AnimatedInterpolation extends AnimatedWithChildren {
|
|
268
|
+
parent;
|
|
269
|
+
config;
|
|
270
|
+
interpolation;
|
|
271
|
+
constructor(parent, config) {
|
|
272
|
+
super();
|
|
273
|
+
this.parent = parent;
|
|
274
|
+
this.config = config;
|
|
275
|
+
// Validate eagerly so a bad range fails at construction, not first frame.
|
|
276
|
+
checkValidRanges(config.inputRange, config.outputRange);
|
|
277
|
+
}
|
|
278
|
+
getInterpolation() {
|
|
279
|
+
if (this.interpolation === undefined) {
|
|
280
|
+
this.interpolation = createInterpolation(this.config);
|
|
281
|
+
}
|
|
282
|
+
return this.interpolation;
|
|
283
|
+
}
|
|
284
|
+
__getValue() {
|
|
285
|
+
const parentValue = this.parent.__getValue();
|
|
286
|
+
if (typeof parentValue !== 'number') {
|
|
287
|
+
throw new Error('Cannot interpolate an input which is not a number');
|
|
288
|
+
}
|
|
289
|
+
return this.getInterpolation()(parentValue);
|
|
290
|
+
}
|
|
291
|
+
__attach() {
|
|
292
|
+
this.parent.__addChild(this);
|
|
293
|
+
super.__attach();
|
|
294
|
+
}
|
|
295
|
+
__detach() {
|
|
296
|
+
this.parent.__removeChild(this);
|
|
297
|
+
super.__detach();
|
|
298
|
+
}
|
|
299
|
+
// Make the upstream value native first, so the parent->interpolation edge can be
|
|
300
|
+
// wired when this node is reached from a leaf rather than from the value.
|
|
301
|
+
__makeNative(platformConfig) {
|
|
302
|
+
this.parent.__makeNative(platformConfig);
|
|
303
|
+
super.__makeNative(platformConfig);
|
|
304
|
+
}
|
|
305
|
+
__getNativeConfig() {
|
|
306
|
+
return {
|
|
307
|
+
type: 'interpolation',
|
|
308
|
+
inputRange: this.config.inputRange,
|
|
309
|
+
outputRange: this.config.outputRange,
|
|
310
|
+
extrapolateLeft: this.config.extrapolateLeft ?? this.config.extrapolate ?? 'extend',
|
|
311
|
+
extrapolateRight: this.config.extrapolateRight ?? this.config.extrapolate ?? 'extend',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export { AnimatedNode, AnimatedWithChildren, flushValue, type IValueListener } from './graph';
|
|
1
|
+
export { AnimatedNode, AnimatedWithChildren, AnimatedInterpolation, flushValue, type IValueListener, } from './graph';
|
|
2
2
|
export { AnimatedValue } from './value';
|
|
3
3
|
export { AnimatedValueXY, type IValueXY } from './value-xy';
|
|
4
|
-
export { AnimatedInterpolation } from './interpolation-node';
|
|
5
4
|
export { AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, add, subtract, multiply, divide, modulo, diffClamp, } from './operators';
|
|
6
5
|
export { AnimatedEvent, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, type IEventConfig, type IEventListener, type IAnimatedEventHandler, type INativeEventAttachment, } from './event';
|
|
7
6
|
export { AnimatedColor, type IRgbaValue, type IColorInput } from './color';
|
|
@@ -15,6 +14,7 @@ export { AnimatedTracking } from './animations/tracking';
|
|
|
15
14
|
export { timing, spring, decay, parallel, sequence, stagger, loop, delay, type ICompositeAnimation, type ITimingConfig, type ISpringConfig, type IDecayConfig, type IParallelConfig, type ILoopAnimationConfig, } from './animations/composition';
|
|
16
15
|
export { nativeAnimated, isNativeAnimatedAvailable, type INativeNodeConfig, type INativeAnimationConfig, type INativeEventMapping, type IPlatformConfig, } from './native/native-animated';
|
|
17
16
|
export { AnimatedProps } from './props';
|
|
17
|
+
export { createAnimatedLeafLifecycle, type IAnimatedLeafLifecycle } from './leaf-lifecycle';
|
|
18
18
|
export { AnimatedStyle, AnimatedTransform } from './style';
|
|
19
19
|
export { AnimatedMock } from './mock';
|
|
20
20
|
export { reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode } from './shared';
|
package/build/animated/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// @symbiote-native/engine/animated: the framework-agnostic, JS-driven Animated engine.
|
|
2
2
|
// The value graph, easing, interpolation and drivers are pure JS with no React
|
|
3
3
|
// and no native dependency; every adapter re-exports them.
|
|
4
|
-
export { AnimatedNode, AnimatedWithChildren, flushValue } from './graph.js';
|
|
4
|
+
export { AnimatedNode, AnimatedWithChildren, AnimatedInterpolation, flushValue, } from './graph.js';
|
|
5
5
|
export { AnimatedValue } from './value.js';
|
|
6
6
|
export { AnimatedValueXY } from './value-xy.js';
|
|
7
|
-
export { AnimatedInterpolation } from './interpolation-node.js';
|
|
8
7
|
export { AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, add, subtract, multiply, divide, modulo, diffClamp, } from './operators.js';
|
|
9
8
|
export { AnimatedEvent, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, } from './event.js';
|
|
10
9
|
export { AnimatedColor } from './color.js';
|
|
@@ -22,6 +21,7 @@ export { nativeAnimated, isNativeAnimatedAvailable, } from './native/native-anim
|
|
|
22
21
|
// no React/Vue). They live here with the rest of the graph; every adapter's
|
|
23
22
|
// createAnimatedComponent + Animated namespace re-exports them.
|
|
24
23
|
export { AnimatedProps } from './props.js';
|
|
24
|
+
export { createAnimatedLeafLifecycle } from './leaf-lifecycle.js';
|
|
25
25
|
export { AnimatedStyle, AnimatedTransform } from './style.js';
|
|
26
26
|
export { AnimatedMock } from './mock.js';
|
|
27
27
|
// Framework-agnostic createAnimatedComponent helpers. Both adapters import them.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// extrapolation. A numeric output range stays number -> number; a string output
|
|
5
5
|
// range (values-with-units like '0deg' or colors like '#000') is interpolated
|
|
6
6
|
// per numeric token and reassembled. See createStringInterpolation below.
|
|
7
|
-
import { normalizeColor } from './
|
|
7
|
+
import { normalizeColor } from './rgba.js';
|
|
8
8
|
import { Easing } from './easing.js';
|
|
9
9
|
function interpolateSegment(input, inputMin, inputMax, outputMin, outputMax, easing, extrapolateLeft, extrapolateRight) {
|
|
10
10
|
let result = input;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ISymbioteNode } from '../node';
|
|
2
|
+
export type IScheduleNativeBind = (bind: () => void) => (() => void) | void;
|
|
3
|
+
export type IAnimatedLeafLifecycle = {
|
|
4
|
+
reconcile(props: Record<string, unknown>, node: ISymbioteNode | null, wantsNative: boolean, scheduleNativeBind?: IScheduleNativeBind): void;
|
|
5
|
+
teardown(): void;
|
|
6
|
+
};
|
|
7
|
+
export declare function createAnimatedLeafLifecycle(label: string): IAnimatedLeafLifecycle;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// The AnimatedProps leaf lifecycle every Animated.* wrapper needs: build a leaf from the current
|
|
2
|
+
// props, swap it into the value graph, bind it to the committed node, go native when asked, and
|
|
3
|
+
// rebind native event props. Framework-agnostic on purpose - it knows nothing about hooks,
|
|
4
|
+
// effects, change detection or reactivity, only the engine's own Animated primitives.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS LIVES IN THE ENGINE. It used to live four times, once per adapter (React's
|
|
7
|
+
// create-animated-component.tsx, Vue's create-animated-component.ts, Svelte's
|
|
8
|
+
// animated-props-runtime.ts, Angular's animated-leaf-binder.ts). Each re-derived the same policy,
|
|
9
|
+
// and they drifted: only Svelte ever grew the rebuild guard below, after a day of device
|
|
10
|
+
// debugging in 2026-08. React's `useMemo(..., [rest])` LOOKS like the same guard but is not - its
|
|
11
|
+
// dependency is a fresh rest-destructured object every render - and Vue's `pendingLeaf === null`
|
|
12
|
+
// answers "has render run yet", not "did anything change". A fix landing in one copy is exactly
|
|
13
|
+
// what <adapters_reach_full_feature_parity> forbids: parity has to be structural.
|
|
14
|
+
//
|
|
15
|
+
// What stays in the adapter: WHEN to call reconcile (an effect, onUpdated, ngOnChanges, a $effect)
|
|
16
|
+
// and HOW to find the host node (a ref, a ViewChild, a shim). Everything below is the same for all.
|
|
17
|
+
import { AnimatedProps } from './props.js';
|
|
18
|
+
import { attachNativeEventHandler } from './event.js';
|
|
19
|
+
import { dlog } from '../debug.js';
|
|
20
|
+
// Diagnostic-only, DEBUG-gated: a process-wide sequence number so every reconcile call across
|
|
21
|
+
// every Animated.* instance in a log dump is individually identifiable and orderable.
|
|
22
|
+
let globalReconcileSeq = 0;
|
|
23
|
+
// Per-key identity comparison, NOT deep equality. A style/handler/props object that is memoized
|
|
24
|
+
// upstream stays `===` across ticks that did not touch it, which is what distinguishes "same
|
|
25
|
+
// content" from a real change. Deep-comparing instead would walk live AnimatedNode graphs, which
|
|
26
|
+
// are circular.
|
|
27
|
+
//
|
|
28
|
+
// The caller MUST pass a stored SNAPSHOT as `a` - see `lastProps` below. Handing this the same
|
|
29
|
+
// live object on both sides compares it with itself and is unconditionally true.
|
|
30
|
+
function shallowEqualProps(a, b) {
|
|
31
|
+
const aKeys = Object.keys(a);
|
|
32
|
+
const bKeys = Object.keys(b);
|
|
33
|
+
if (aKeys.length !== bKeys.length)
|
|
34
|
+
return false;
|
|
35
|
+
for (const key of aKeys) {
|
|
36
|
+
if (a[key] !== b[key])
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
// Diagnostic-only. `style` here is a raw prop value, so it can be anything the caller wrote -
|
|
42
|
+
// an object, an array of them, a class name, undefined. Read the field, do not assume a shape.
|
|
43
|
+
function readTransform(style) {
|
|
44
|
+
if (typeof style !== 'object' || style === null)
|
|
45
|
+
return undefined;
|
|
46
|
+
return Reflect.get(style, 'transform');
|
|
47
|
+
}
|
|
48
|
+
// Diagnostic-only: a SHALLOW, non-serializing description of a style.transform array. Props here
|
|
49
|
+
// carry RAW values - an animated entry is a live AnimatedNode in a circular parent<->children
|
|
50
|
+
// graph - so JSON.stringify throws "Converting circular structure to JSON". Describe by
|
|
51
|
+
// constructor name instead of walking it.
|
|
52
|
+
function describeTransform(value) {
|
|
53
|
+
if (!Array.isArray(value))
|
|
54
|
+
return String(value);
|
|
55
|
+
const entries = value.map(entry => {
|
|
56
|
+
if (typeof entry !== 'object' || entry === null)
|
|
57
|
+
return String(entry);
|
|
58
|
+
const [key] = Object.keys(entry);
|
|
59
|
+
const inner = key === undefined ? undefined : Reflect.get(entry, key);
|
|
60
|
+
const innerDesc = typeof inner === 'number' || typeof inner === 'string'
|
|
61
|
+
? String(inner)
|
|
62
|
+
: `<${inner !== null && typeof inner === 'object' ? inner.constructor.name : typeof inner}>`;
|
|
63
|
+
return `${key}:${innerDesc}`;
|
|
64
|
+
});
|
|
65
|
+
return `[${entries.join(',')}]`;
|
|
66
|
+
}
|
|
67
|
+
export function createAnimatedLeafLifecycle(label) {
|
|
68
|
+
let attached = null;
|
|
69
|
+
let eventDetachers = [];
|
|
70
|
+
// Diagnostic-only: the last node identity, plus a reentrancy flag to catch reconcile() being
|
|
71
|
+
// called AGAIN from inside its own call stack - the smoking-gun shape for a synchronous
|
|
72
|
+
// same-flush loop, as opposed to merely "called often".
|
|
73
|
+
let lastNode = null;
|
|
74
|
+
let inReconcile = false;
|
|
75
|
+
// Last-seen props/wantsNative for the skip below - a real content check, not diagnostics.
|
|
76
|
+
let lastProps = null;
|
|
77
|
+
let lastWantsNative = false;
|
|
78
|
+
// Canceller for a native bind the caller deferred and that has not run yet.
|
|
79
|
+
let cancelPendingBind;
|
|
80
|
+
function detachEvents() {
|
|
81
|
+
for (const detach of eventDetachers)
|
|
82
|
+
detach();
|
|
83
|
+
eventDetachers = [];
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
reconcile(props, node, wantsNative, scheduleNativeBind) {
|
|
87
|
+
const seq = ++globalReconcileSeq;
|
|
88
|
+
const reentrant = inReconcile;
|
|
89
|
+
const nodeChanged = node !== lastNode;
|
|
90
|
+
const wantsNativeChanged = wantsNative !== lastWantsNative;
|
|
91
|
+
const propsChanged = lastProps === null || !shallowEqualProps(lastProps, props);
|
|
92
|
+
lastNode = node;
|
|
93
|
+
lastWantsNative = wantsNative;
|
|
94
|
+
// A SNAPSHOT, never the reference. A caller can legitimately hand back the SAME object on
|
|
95
|
+
// every update, mutating what its keys resolve to rather than allocating (Svelte's rest
|
|
96
|
+
// proxy does exactly that, device-confirmed 2026-08-13); storing it would make the next
|
|
97
|
+
// call run shallowEqualProps on that object against ITSELF - reading the same current values
|
|
98
|
+
// through both sides - which is unconditionally true. `propsChanged` could then never be
|
|
99
|
+
// true again and reconcile would be skipped forever, so every rebuilt AnimatedInterpolation
|
|
100
|
+
// never reaches the native graph: the view stays wired to the FIRST interpolation, built
|
|
101
|
+
// before measurement with range [-1,0]->[0,0]. One pixel of travel, which on device reads as
|
|
102
|
+
// "the sticky header ignores scrolling entirely".
|
|
103
|
+
lastProps = { ...props };
|
|
104
|
+
// Thunk, not a string: reconcile runs on EVERY update of every Animated.* wrapper in the
|
|
105
|
+
// app (Angular re-reads it on each change-detection pass), and describeTransform allocates.
|
|
106
|
+
// Built eagerly it would cost its full price with logging off.
|
|
107
|
+
dlog(() => `AnimatedProps[${label}] reconcile#${seq} reentrant=${reentrant} wantsNative=${wantsNative} ` +
|
|
108
|
+
`hasNode=${node !== null} nodeChanged=${nodeChanged} propsChanged=${propsChanged} ` +
|
|
109
|
+
`transform=${describeTransform(readTransform(props.style))}`);
|
|
110
|
+
if (reentrant) {
|
|
111
|
+
dlog(`AnimatedProps[${label}] reconcile#${seq} *** RE-ENTRANT CALL DETECTED - see reconcile above ***`);
|
|
112
|
+
}
|
|
113
|
+
// Nothing meaningfully changed since the last call AND we are already natively connected
|
|
114
|
+
// (steady state) - skip tearing down and rebuilding the native AnimatedProps graph.
|
|
115
|
+
//
|
|
116
|
+
// Deliberately scoped to `attached.__isNative()` ONLY: before the first native connection,
|
|
117
|
+
// reconcile must run unconditionally on every tick - that cadence is what wires a rebuilt
|
|
118
|
+
// interpolation node into the shared value's children (AnimatedInterpolation.__attach ->
|
|
119
|
+
// parent.__addChild, cascaded from AnimatedProps.__attach); skipping there can leave a
|
|
120
|
+
// freshly rebuilt interpolation never attached, so its listener never fires. Once native and
|
|
121
|
+
// staying native, that bootstrap concern is moot - and rebuilding there was the
|
|
122
|
+
// collision-disappearance bug: every scroll-driven passthrough tick reconnected a brand new
|
|
123
|
+
// native node (restoreDefaultValues + connect) even though nothing animated-relevant had
|
|
124
|
+
// changed, and a reconnect landing exactly when scrolling stopped left the view frozen at
|
|
125
|
+
// its post-reset default until the next tick.
|
|
126
|
+
if (attached !== null &&
|
|
127
|
+
attached.__isNative() &&
|
|
128
|
+
!nodeChanged &&
|
|
129
|
+
!wantsNativeChanged &&
|
|
130
|
+
!propsChanged) {
|
|
131
|
+
dlog(`AnimatedProps[${label}] reconcile#${seq} skipped (no-op: already native, props/node unchanged)`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
inReconcile = true;
|
|
135
|
+
try {
|
|
136
|
+
// Attach the NEW leaf BEFORE detaching the OLD one: a shared Value self-detaches (dropping
|
|
137
|
+
// its native node) the instant its child count hits zero, so detaching first would kill a
|
|
138
|
+
// running native animation on any unrelated update. Mirrors RN's
|
|
139
|
+
// AnimatedComponent._attachProps.
|
|
140
|
+
const newLeaf = new AnimatedProps(props);
|
|
141
|
+
newLeaf.__attach();
|
|
142
|
+
if (attached !== null && attached !== newLeaf)
|
|
143
|
+
attached.__detach();
|
|
144
|
+
attached = newLeaf;
|
|
145
|
+
// The native half, which a caller may defer. Rebinds events each reconcile so a new inline
|
|
146
|
+
// event re-attaches, detaching first so the prior binding does not leak;
|
|
147
|
+
// attachNativeEventHandler no-ops unless the prop really is a native event handler on a
|
|
148
|
+
// committed node, so the JS path stays the fallback.
|
|
149
|
+
const bindNative = () => {
|
|
150
|
+
if (node !== null)
|
|
151
|
+
newLeaf.setNativeView(node);
|
|
152
|
+
if (wantsNative)
|
|
153
|
+
newLeaf.__makeNative();
|
|
154
|
+
detachEvents();
|
|
155
|
+
if (node === null)
|
|
156
|
+
return;
|
|
157
|
+
for (const key of Object.keys(props)) {
|
|
158
|
+
const attachment = attachNativeEventHandler(node, key, props[key]);
|
|
159
|
+
if (attachment !== undefined)
|
|
160
|
+
eventDetachers.push(attachment.detach);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
cancelPendingBind?.();
|
|
164
|
+
cancelPendingBind = undefined;
|
|
165
|
+
if (scheduleNativeBind === undefined || node === null) {
|
|
166
|
+
bindNative();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
cancelPendingBind = scheduleNativeBind(bindNative) ?? undefined;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
inReconcile = false;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
teardown() {
|
|
176
|
+
cancelPendingBind?.();
|
|
177
|
+
cancelPendingBind = undefined;
|
|
178
|
+
detachEvents();
|
|
179
|
+
if (attached !== null) {
|
|
180
|
+
attached.__detach();
|
|
181
|
+
attached = null;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -101,6 +101,7 @@ export const nativeAnimated = {
|
|
|
101
101
|
module()?.stopAnimation(animationId);
|
|
102
102
|
},
|
|
103
103
|
setAnimatedNodeValue(nodeTag, value) {
|
|
104
|
+
dlog(`native: setAnimatedNodeValue node=${nodeTag} value=${value}`);
|
|
104
105
|
module()?.setAnimatedNodeValue(nodeTag, value);
|
|
105
106
|
},
|
|
106
107
|
setAnimatedNodeOffset(nodeTag, offset) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Pure RGBA parsing — no graph node, no AnimatedValue, no imports from this package at all.
|
|
2
|
+
//
|
|
3
|
+
// It is split out of color.ts DELIBERATELY, and must stay dependency-free. `interpolation.ts`
|
|
4
|
+
// needs `normalizeColor` for its color output ranges, and `graph.ts` needs `interpolation.ts`
|
|
5
|
+
// (AnimatedInterpolation lives there). color.ts also defines `AnimatedColor extends
|
|
6
|
+
// AnimatedWithChildren`, so leaving the parser there closes a cycle
|
|
7
|
+
// graph -> interpolation -> color -> graph
|
|
8
|
+
// which surfaces at load time as `TypeError: Class extends value undefined is not a
|
|
9
|
+
// constructor` — AnimatedWithChildren is still uninitialized when color.ts evaluates. Keeping
|
|
10
|
+
// the pure half in its own leaf module breaks it structurally rather than by import ordering.
|
|
11
|
+
export const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 };
|
|
12
|
+
// Decompose a #hex (3/4/6/8), rgb()/rgba(), or 0xRRGGBBAA number into channels.
|
|
13
|
+
// undefined when unparseable (a named/platform color), so the caller falls back to
|
|
14
|
+
// the default rather than throwing inside a render. Exported so interpolation's
|
|
15
|
+
// color path parses through the same RGBA decoder (DRY) rather than duplicating it.
|
|
16
|
+
export function normalizeColor(color) {
|
|
17
|
+
if (typeof color === 'number') {
|
|
18
|
+
const c = color >>> 0;
|
|
19
|
+
return { r: (c >>> 24) & 255, g: (c >>> 16) & 255, b: (c >>> 8) & 255, a: (c & 255) / 255 };
|
|
20
|
+
}
|
|
21
|
+
const trimmed = color.trim();
|
|
22
|
+
if (trimmed.startsWith('#'))
|
|
23
|
+
return parseHex(trimmed);
|
|
24
|
+
if (/^rgba?\(/i.test(trimmed))
|
|
25
|
+
return parseRgb(trimmed);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function parseHex(hex) {
|
|
29
|
+
let body = hex.slice(1);
|
|
30
|
+
if (body.length === 3 || body.length === 4) {
|
|
31
|
+
body = body
|
|
32
|
+
.split('')
|
|
33
|
+
.map(c => c + c)
|
|
34
|
+
.join('');
|
|
35
|
+
}
|
|
36
|
+
if (body.length !== 6 && body.length !== 8)
|
|
37
|
+
return undefined;
|
|
38
|
+
const int = Number.parseInt(body, 16);
|
|
39
|
+
if (Number.isNaN(int))
|
|
40
|
+
return undefined;
|
|
41
|
+
if (body.length === 6) {
|
|
42
|
+
return { r: (int >>> 16) & 255, g: (int >>> 8) & 255, b: int & 255, a: 1 };
|
|
43
|
+
}
|
|
44
|
+
const c = int >>> 0;
|
|
45
|
+
return { r: (c >>> 24) & 255, g: (c >>> 16) & 255, b: (c >>> 8) & 255, a: (c & 255) / 255 };
|
|
46
|
+
}
|
|
47
|
+
function parseRgb(str) {
|
|
48
|
+
const match = /^rgba?\(([^)]+)\)$/i.exec(str);
|
|
49
|
+
if (match === null)
|
|
50
|
+
return undefined;
|
|
51
|
+
const parts = match[1].split(',').map(part => Number.parseFloat(part.trim()));
|
|
52
|
+
if (parts.length < 3 || parts.some(n => Number.isNaN(n)))
|
|
53
|
+
return undefined;
|
|
54
|
+
return { r: parts[0], g: parts[1], b: parts[2], a: parts.length >= 4 ? parts[3] : 1 };
|
|
55
|
+
}
|