@symbiote-native/engine 0.4.0 → 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/README.md +39 -14
- package/android/CMakeLists.txt +51 -0
- package/android/build.gradle +90 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
- package/build/accessibility-info/shared.js +1 -1
- package/build/accessibility-props.d.ts +1 -7
- package/build/accessibility-props.js +22 -21
- package/build/animated/animations/composition.d.ts +1 -1
- package/build/animated/animations/composition.js +18 -4
- package/build/animated/easing.d.ts +3 -2
- package/build/animated/easing.js +17 -88
- package/build/animated/event.js +6 -1
- package/build/animated/graph.d.ts +2 -0
- package/build/animated/graph.js +14 -0
- package/build/animated/host-binding.d.ts +39 -0
- package/build/animated/host-binding.js +278 -0
- package/build/animated/index.d.ts +1 -1
- package/build/animated/leaf-lifecycle.js +10 -22
- package/build/animated/mock.d.ts +1 -19
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +16 -50
- package/build/events/index.js +123 -33
- package/build/fabric-props.d.ts +1 -1
- package/build/fabric-props.js +129 -182
- package/build/fabric.d.ts +10 -0
- package/build/fabric.js +40 -0
- package/build/host-access.d.ts +125 -0
- package/build/host-access.js +280 -0
- package/build/host-behavior.d.ts +107 -7
- package/build/host-behavior.js +309 -31
- package/build/image-source-write.d.ts +16 -0
- package/build/image-source-write.js +65 -0
- package/build/imperative.d.ts +49 -0
- package/build/imperative.js +258 -0
- package/build/index.d.ts +18 -10
- package/build/index.js +65 -10
- package/build/mutation-buffer.d.ts +222 -0
- package/build/mutation-buffer.js +491 -0
- package/build/native-engine.d.ts +182 -0
- package/build/native-engine.js +178 -0
- package/build/native-tree-host.d.ts +25 -0
- package/build/native-tree-host.js +66 -0
- package/build/node.d.ts +191 -60
- package/build/node.js +1034 -327
- package/build/pan-responder/index.d.ts +2 -2
- package/build/pan-responder/index.js +37 -56
- package/build/platform-color/index.d.ts +1 -1
- package/build/platform-color/index.js +11 -4
- package/build/process-background-image/index.js +30 -566
- package/build/process-background-longhands.d.ts +4 -0
- package/build/process-background-longhands.js +44 -0
- package/build/process-box-shadow/index.js +23 -187
- package/build/process-filter.js +27 -300
- package/build/process-transform/index.d.ts +1 -1
- package/build/process-transform/index.js +25 -107
- package/build/process-transform-origin/index.d.ts +1 -1
- package/build/process-transform-origin/index.js +29 -102
- package/build/registry.d.ts +36 -0
- package/build/registry.js +73 -0
- package/build/sound-manager/index.d.ts +3 -0
- package/build/sound-manager/index.js +36 -0
- package/build/structured-style.d.ts +10 -0
- package/build/structured-style.js +180 -0
- package/build/style-registry/index.d.ts +14 -0
- package/build/style-registry/index.js +60 -11
- package/build/styles.d.ts +5 -1
- package/build/surface.d.ts +31 -2
- package/build/surface.js +138 -42
- package/build/text-input-state.d.ts +1 -0
- package/build/text-input-state.js +17 -3
- package/build/tree-host.d.ts +307 -0
- package/build/tree-host.js +211 -0
- package/build/view-config.js +4 -4
- package/codegen-specs/NativeSymbioteEngine.ts +27 -0
- package/cpp/SymbioteDebug.cpp +51 -0
- package/cpp/SymbioteDebug.h +54 -0
- package/cpp/SymbioteEngineBindings.cpp +232 -0
- package/cpp/SymbioteEngineBindings.h +59 -0
- package/cpp/SymbioteFabricProps.cpp +2619 -0
- package/cpp/SymbioteFabricProps.h +223 -0
- package/cpp/SymbioteTree.cpp +2478 -0
- package/cpp/SymbioteTree.h +257 -0
- package/ios/SymbioteEngineModule.h +25 -0
- package/ios/SymbioteEngineModule.mm +44 -0
- package/package.json +31 -3
- package/react-native.config.cjs +23 -0
- package/symbiote-engine.podspec +42 -0
- package/build/animated/bezier.d.ts +0 -1
- package/build/animated/bezier.js +0 -102
- package/build/commit.d.ts +0 -49
- package/build/commit.js +0 -1030
- package/build/tags.d.ts +0 -2
- package/build/tags.js +0 -40
package/build/animated/easing.js
CHANGED
|
@@ -1,88 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (cachedEase === undefined) {
|
|
19
|
-
cachedEase = bezier(0.42, 0, 1, 1);
|
|
20
|
-
}
|
|
21
|
-
return cachedEase(t);
|
|
22
|
-
},
|
|
23
|
-
quad(t) {
|
|
24
|
-
return t * t;
|
|
25
|
-
},
|
|
26
|
-
cubic(t) {
|
|
27
|
-
return t * t * t;
|
|
28
|
-
},
|
|
29
|
-
// Nth-power: position is the Nth power of elapsed time.
|
|
30
|
-
poly(n) {
|
|
31
|
-
return t => Math.pow(t, n);
|
|
32
|
-
},
|
|
33
|
-
sin(t) {
|
|
34
|
-
return 1 - Math.cos((t * Math.PI) / 2);
|
|
35
|
-
},
|
|
36
|
-
circle(t) {
|
|
37
|
-
return 1 - Math.sqrt(1 - t * t);
|
|
38
|
-
},
|
|
39
|
-
exp(t) {
|
|
40
|
-
return Math.pow(2, 10 * (t - 1));
|
|
41
|
-
},
|
|
42
|
-
// Elastic: spring-like oscillation. bounciness 1 overshoots once; 0 not at all.
|
|
43
|
-
elastic(bounciness = 1) {
|
|
44
|
-
const p = bounciness * Math.PI;
|
|
45
|
-
return t => 1 - Math.pow(Math.cos((t * Math.PI) / 2), 3) * Math.cos(t * p);
|
|
46
|
-
},
|
|
47
|
-
// Animates back slightly before moving forward.
|
|
48
|
-
back(s = 1.70158) {
|
|
49
|
-
return t => t * t * ((s + 1) * t - s);
|
|
50
|
-
},
|
|
51
|
-
// A simple bouncing effect.
|
|
52
|
-
bounce(t) {
|
|
53
|
-
if (t < 1 / 2.75) {
|
|
54
|
-
return 7.5625 * t * t;
|
|
55
|
-
}
|
|
56
|
-
if (t < 2 / 2.75) {
|
|
57
|
-
const t2 = t - 1.5 / 2.75;
|
|
58
|
-
return 7.5625 * t2 * t2 + 0.75;
|
|
59
|
-
}
|
|
60
|
-
if (t < 2.5 / 2.75) {
|
|
61
|
-
const t2 = t - 2.25 / 2.75;
|
|
62
|
-
return 7.5625 * t2 * t2 + 0.9375;
|
|
63
|
-
}
|
|
64
|
-
const t2 = t - 2.625 / 2.75;
|
|
65
|
-
return 7.5625 * t2 * t2 + 0.984375;
|
|
66
|
-
},
|
|
67
|
-
// A cubic bezier curve, same as CSS transition-timing-function.
|
|
68
|
-
bezier(x1, y1, x2, y2) {
|
|
69
|
-
return bezier(x1, y1, x2, y2);
|
|
70
|
-
},
|
|
71
|
-
// Runs an easing function forwards.
|
|
72
|
-
in(easing) {
|
|
73
|
-
return easing;
|
|
74
|
-
},
|
|
75
|
-
// Runs an easing function backwards.
|
|
76
|
-
out(easing) {
|
|
77
|
-
return t => 1 - easing(1 - t);
|
|
78
|
-
},
|
|
79
|
-
// Makes any easing function symmetrical (forwards then backwards).
|
|
80
|
-
inOut(easing) {
|
|
81
|
-
return t => {
|
|
82
|
-
if (t < 0.5) {
|
|
83
|
-
return easing(t * 2) / 2;
|
|
84
|
-
}
|
|
85
|
-
return 1 - easing((1 - t) * 2) / 2;
|
|
86
|
-
};
|
|
87
|
-
},
|
|
88
|
-
};
|
|
1
|
+
// RN's own Easing, imported rather than ported.
|
|
2
|
+
//
|
|
3
|
+
// The port this replaces was 109 lines beside a 142-line `bezier.ts`, and it reproduced upstream
|
|
4
|
+
// formula for formula. Swept at 20 001 points per function before the swap: 15 of the 17 agree to
|
|
5
|
+
// the bit, and the two bezier-backed ones (`ease`, `bezier`) differ by 2.9e-10, because upstream
|
|
6
|
+
// samples its spline into a Float32Array where the port used a plain (float64) array. That is
|
|
7
|
+
// below a sub-pixel on any animation this drives.
|
|
8
|
+
//
|
|
9
|
+
// The TYPE is ours and stays ours. `Easing` is on the engine barrel and re-exported by all five
|
|
10
|
+
// adapters, and the upstream module ships no types - so importing it untyped would hand every
|
|
11
|
+
// consumer `any`. Twenty lines of interface in exchange for 251 lines of implementation.
|
|
12
|
+
import EasingUpstream from 'react-native/Libraries/Animated/Easing';
|
|
13
|
+
// tsc resolves this deep path but reads the Flow source through allowJs, so the default export
|
|
14
|
+
// arrives shapeless rather than untyped - the mismatch is the only thing the suppression covers,
|
|
15
|
+
// and IEasing above is what every consumer actually sees.
|
|
16
|
+
// @ts-expect-error - upstream ships no usable types for this path.
|
|
17
|
+
export const Easing = EasingUpstream;
|
package/build/animated/event.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// and registers the key path with the stock native module, so the event drives
|
|
15
15
|
// the view with zero JS per event.
|
|
16
16
|
import { dlog } from '../debug.js';
|
|
17
|
-
import { getNativeTag, whenCommitted } from '../
|
|
17
|
+
import { getNativeTag, whenCommitted } from '../imperative.js';
|
|
18
18
|
import { isSymbioteNode } from '../node.js';
|
|
19
19
|
import { AnimatedNode, flushValue } from './graph.js';
|
|
20
20
|
import { isNativeAnimatedAvailable, nativeAnimated, } from './native/native-animated.js';
|
|
@@ -45,6 +45,11 @@ function collectMappedValues(mapping, path, out) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
// TODO(rn-parity, low priority): vendor's `AnimatedEvent.js` has `validateMapping`, a
|
|
49
|
+
// `__DEV__`-only check that throws when an event's actual shape doesn't match the argMapping
|
|
50
|
+
// structure. We have no equivalent — `extractAtPath` below silently no-ops on a mismatch instead.
|
|
51
|
+
// Judged non-actionable (misuse-detection guard, no runtime-visible effect for a correct mapping,
|
|
52
|
+
// same class as the `numColumns` invariant) — see the audit skill for the full reasoning.
|
|
48
53
|
// Pull the numeric field at `path` out of one event argument. Returns undefined
|
|
49
54
|
// when the path is absent or the leaf is not a number, so a malformed event is a
|
|
50
55
|
// no-op rather than a throw inside an event dispatch.
|
|
@@ -3,12 +3,14 @@ import { type IInterpolationConfig } from './interpolation';
|
|
|
3
3
|
export type IValueListener = (state: {
|
|
4
4
|
value: number | string;
|
|
5
5
|
}) => void;
|
|
6
|
+
export declare function hasAnimatedNodes(): boolean;
|
|
6
7
|
export declare class AnimatedNode {
|
|
7
8
|
private readonly listeners;
|
|
8
9
|
private suspendCallbacks;
|
|
9
10
|
protected isNative: boolean;
|
|
10
11
|
private nativeTag;
|
|
11
12
|
private platformConfig;
|
|
13
|
+
constructor();
|
|
12
14
|
__attach(): void;
|
|
13
15
|
__detach(): void;
|
|
14
16
|
__isNative(): boolean;
|
package/build/animated/graph.js
CHANGED
|
@@ -20,6 +20,17 @@ let nextListenerId = 1;
|
|
|
20
20
|
// channel. RN tolerates the per-channel flushes and relies on a downstream commit-
|
|
21
21
|
// coalescing layer; symbiote has none here, so it coalesces at the source.
|
|
22
22
|
let flushSuspendDepth = 0;
|
|
23
|
+
// Has this process ever constructed an Animated node at all?
|
|
24
|
+
//
|
|
25
|
+
// `routeProp` has to answer "does this prop hold an animated value" on EVERY prop write - 32 001
|
|
26
|
+
// of them on one benchmark create - and for an app that animates nothing the only cheap answer is
|
|
27
|
+
// that no AnimatedNode exists to find. One boolean read, the discipline `hasHostBehaviors` and
|
|
28
|
+
// `isDebug` already set. Set in the base constructor so every node type raises it, including the
|
|
29
|
+
// operators and interpolations an app never names directly.
|
|
30
|
+
let anyAnimatedNode = false;
|
|
31
|
+
export function hasAnimatedNodes() {
|
|
32
|
+
return anyAnimatedNode;
|
|
33
|
+
}
|
|
23
34
|
export class AnimatedNode {
|
|
24
35
|
listeners = new Map();
|
|
25
36
|
// While > 0, this node's own __callListeners is a no-op. A composite setter
|
|
@@ -39,6 +50,9 @@ export class AnimatedNode {
|
|
|
39
50
|
// native config at creation (__getNativeTag). Optional: undefined when no caller
|
|
40
51
|
// supplied one, matching today's behavior.
|
|
41
52
|
platformConfig;
|
|
53
|
+
constructor() {
|
|
54
|
+
anyAnimatedNode = true;
|
|
55
|
+
}
|
|
42
56
|
__attach() { }
|
|
43
57
|
__detach() {
|
|
44
58
|
this.removeAllListeners();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ISymbioteNode } from '../node';
|
|
2
|
+
export declare function hasAnimatedBindings(): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Give a node a behavior-owned animated style layer, or drop it by passing `undefined`.
|
|
5
|
+
*
|
|
6
|
+
* The seam `<touchable-opacity>` needs: RN runs its press fade from an `Animated.View` whose
|
|
7
|
+
* style is `[props.style, {opacity: anim}]` (TouchableOpacity.js:302), and a tag has no such
|
|
8
|
+
* wrapper. The layer is bound to the leaf, never folded into `node.props.style` — so the behavior
|
|
9
|
+
* can still read the AUTHOR's resting opacity back without seeing its own fade.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setAnimatedBehaviorStyle(node: ISymbioteNode, style: unknown): void;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve an animated value in a prop, returning what should be published for it.
|
|
14
|
+
*
|
|
15
|
+
* Returns its input by IDENTITY when the value holds nothing animated, so `routeProp` can call it
|
|
16
|
+
* unconditionally and every downstream branch — class, style, activeStyle, `on*`, `setProp` —
|
|
17
|
+
* keeps seeing a plain value and needs no change.
|
|
18
|
+
*/
|
|
19
|
+
export declare function bindAnimatedValue(node: ISymbioteNode, key: string, value: unknown): unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Bind a native-driven `Animated.event` handler written as an `on*` prop. A no-op for every other
|
|
22
|
+
* handler, so `routeProp` calls it for any `on*` name behind the same one-boolean gate.
|
|
23
|
+
*/
|
|
24
|
+
export declare function bindAnimatedEvent(node: ISymbioteNode, propName: string, handler: unknown): void;
|
|
25
|
+
/**
|
|
26
|
+
* Release a node's animated subscription. Called per node from the commit sweep, which is where a
|
|
27
|
+
* genuine removal is first distinguishable from a framework spelling a move as remove-then-insert
|
|
28
|
+
* (host-behavior.ts, `markDetachCandidate`).
|
|
29
|
+
*/
|
|
30
|
+
export declare function detachAnimatedProps(node: ISymbioteNode): void;
|
|
31
|
+
/**
|
|
32
|
+
* Re-arm a subtree the sweep tore down and the framework put back — Svelte parks LIVE nodes
|
|
33
|
+
* offscreen across commits and returns them with their props unwritten, so without this the
|
|
34
|
+
* animation stops with nothing red anywhere.
|
|
35
|
+
*
|
|
36
|
+
* A WeakSet miss and no walk at all for every node in a freshly built tree, which is the path that
|
|
37
|
+
* runs ~9 000 times per benchmark create.
|
|
38
|
+
*/
|
|
39
|
+
export declare function reattachAnimatedProps(node: ISymbioteNode): void;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// An AnimatedNode written straight into a prop of a HOST NODE — the engine half of what
|
|
2
|
+
// `createAnimatedComponent` used to broker.
|
|
3
|
+
//
|
|
4
|
+
// WHY IT EXISTS. `Animated.View` is `createAnimatedComponent(View)`: a wrapper whose whole job is
|
|
5
|
+
// to keep an `AnimatedProps` leaf alive beside a base COMPONENT. Once a primitive is a bare
|
|
6
|
+
// intrinsic tag there is no component to wrap, so every `Animated.*` built that way loses its
|
|
7
|
+
// base. The two halves the wrapper actually brokered — the value graph and the node's Fabric view
|
|
8
|
+
// tag — are both already engine-side (`animated/props.ts`, `commit.ts`'s `getNativeTag`), so the
|
|
9
|
+
// wrapper was standing between two things that live in the same room. An app writes
|
|
10
|
+
//
|
|
11
|
+
// <view style={{ opacity: someAnimatedValue }} />
|
|
12
|
+
//
|
|
13
|
+
// and `routeProp` resolves it here: publish the current value so the first paint is concrete,
|
|
14
|
+
// subscribe the leaf so every frame lands as one targeted `setNativeProps` commit, and tear the
|
|
15
|
+
// subscription down when the node leaves the tree for good.
|
|
16
|
+
//
|
|
17
|
+
// THE PRECEDENT IT FOLLOWS is `routeProp`'s own `isStyleCallback` branch: a `style` function is
|
|
18
|
+
// already a value the engine INTERPRETS rather than forwards, resolved at both values of
|
|
19
|
+
// `pressed`. An AnimatedNode is the same shape of problem one step further — the resolution is
|
|
20
|
+
// continuous rather than two-valued, so it needs a subscription instead of a second call.
|
|
21
|
+
//
|
|
22
|
+
// THE CYCLE IS DELIBERATE, and it is the one `node.ts` already carries with `commit.ts`: node.ts
|
|
23
|
+
// imports this module for `routeProp`, and this module reaches back into node.ts for `setProp`.
|
|
24
|
+
// Neither side touches the other at module-evaluation time — only inside a function body — so
|
|
25
|
+
// every loader resolves it. The alternative, a
|
|
26
|
+
// `registerAnimatedResolver` installed from elsewhere, is exactly the load-time-side-effect shape
|
|
27
|
+
// Metro's `inlineRequires` drops in release builds (CLAUDE.md; and see `graph.ts`'s note on why
|
|
28
|
+
// `AnimatedInterpolation` lives next to its base class).
|
|
29
|
+
import { AnimatedNode } from './graph.js';
|
|
30
|
+
import { AnimatedStyle } from './style.js';
|
|
31
|
+
import { attachNativeEventHandler } from './event.js';
|
|
32
|
+
import { createAnimatedLeafLifecycle, } from './leaf-lifecycle.js';
|
|
33
|
+
import { setProp } from '../node.js';
|
|
34
|
+
import { propOf, subtreesOf } from '../host-access.js';
|
|
35
|
+
const bindings = new WeakMap();
|
|
36
|
+
// Nodes the commit sweep tore down, whether or not they carried a binding — the twin of
|
|
37
|
+
// host-behavior.ts's `tornDown`, and marked for every node for the same reason: the node a
|
|
38
|
+
// framework re-inserts is usually a plain container whose DESCENDANT holds the subscription.
|
|
39
|
+
const parked = new WeakSet();
|
|
40
|
+
// The gate, matching `hasHostBehaviors`. `removeChild` and the two inserts read it on every call,
|
|
41
|
+
// so an app that animates nothing must pay one boolean read rather than a WeakMap probe.
|
|
42
|
+
let anyBinding = false;
|
|
43
|
+
export function hasAnimatedBindings() {
|
|
44
|
+
return anyBinding;
|
|
45
|
+
}
|
|
46
|
+
// Only what `AnimatedProps` can actually bind: a prop that IS a node, or a `style` holding one.
|
|
47
|
+
// Deliberately NOT a general deep walk — an arbitrary prop bag can hold an app object with a
|
|
48
|
+
// cycle in it, and a walk that never terminates would be a hang on the engine's hottest path.
|
|
49
|
+
function styleHoldsAnimated(style) {
|
|
50
|
+
if (Array.isArray(style))
|
|
51
|
+
return style.some(styleHoldsAnimated);
|
|
52
|
+
if (typeof style !== 'object' || style === null)
|
|
53
|
+
return false;
|
|
54
|
+
for (const key of Object.keys(style)) {
|
|
55
|
+
const entry = Reflect.get(style, key);
|
|
56
|
+
if (entry instanceof AnimatedNode)
|
|
57
|
+
return true;
|
|
58
|
+
// `transform: [{ translateX: node }]` — the one nesting `AnimatedTransform` reads.
|
|
59
|
+
if (key !== 'transform' || !Array.isArray(entry))
|
|
60
|
+
continue;
|
|
61
|
+
for (const item of entry) {
|
|
62
|
+
if (typeof item !== 'object' || item === null)
|
|
63
|
+
continue;
|
|
64
|
+
for (const inner of Object.keys(item)) {
|
|
65
|
+
if (Reflect.get(item, inner) instanceof AnimatedNode)
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// The value to PUBLISH now, or undefined when there is nothing animated after all. A false
|
|
73
|
+
// positive from the scan above lands here and is turned away, so the raw value still reaches
|
|
74
|
+
// `setProp` unchanged and nothing new can break.
|
|
75
|
+
function rasterize(key, value) {
|
|
76
|
+
if (value instanceof AnimatedNode)
|
|
77
|
+
return value.__getValue();
|
|
78
|
+
if (key !== 'style')
|
|
79
|
+
return undefined;
|
|
80
|
+
return AnimatedStyle.from(value)?.__getValue();
|
|
81
|
+
}
|
|
82
|
+
function reconcile(node, binding) {
|
|
83
|
+
parked.delete(node);
|
|
84
|
+
if (Object.keys(binding.raw).length > 0) {
|
|
85
|
+
// `wantsNative: false` — nothing here forces the native driver. A leaf becomes native by
|
|
86
|
+
// CASCADE, from the value it is a child of (`AnimatedWithChildren.__addChild` /
|
|
87
|
+
// `__connectNativeChildren`), which is what makes `useNativeDriver` on the animation the only
|
|
88
|
+
// thing that decides. The wrapper's own `wantsNative` was keyed on
|
|
89
|
+
// `passthroughAnimatedPropExplicitValues`, a wrapper-ism with no meaning on a bare tag.
|
|
90
|
+
//
|
|
91
|
+
// And NO `scheduleNativeBind`, though a prop write always precedes the node's first commit:
|
|
92
|
+
// nothing the bind does needs a Fabric tag on the spot. `setNativeView` only stores the
|
|
93
|
+
// target, `connectToView` defers itself through `pendingViewConnects` + the post-commit hook
|
|
94
|
+
// (`props.ts`), and `attachNativeEventHandler` wraps its own `whenCommitted`. A deferral here
|
|
95
|
+
// would be unfalsifiable code, so it is not here.
|
|
96
|
+
binding.lifecycle.reconcile(binding.raw, node, false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
binding.lifecycle.teardown();
|
|
100
|
+
bindings.delete(node);
|
|
101
|
+
// Fabric may flatten a view again once nothing animates it.
|
|
102
|
+
setProp(node, 'collapsable', undefined);
|
|
103
|
+
}
|
|
104
|
+
// A style layer a HOST BEHAVIOR owns on its own node, composed OVER the app's style.
|
|
105
|
+
//
|
|
106
|
+
// WHY IT IS NOT AN ORDINARY PROP. TouchableOpacity's press fade is an AnimatedValue that has to
|
|
107
|
+
// beat whatever `opacity` the caller's own style asks for, and `fabricProps` hoists the style slot
|
|
108
|
+
// AFTER every plain prop — so a top-level `opacity` loses to `style={{opacity: 0.6}}`, silently and
|
|
109
|
+
// only for the styled call sites. The layer has to sit INSIDE the style, above the author's.
|
|
110
|
+
//
|
|
111
|
+
// AND IT CANNOT SHARE `raw.style` WITH THE APP. The leaf keys its bound props by name, so the app's
|
|
112
|
+
// next plain `style` write would find `raw.style` holding nothing animated and tear the behavior's
|
|
113
|
+
// binding down. Composing here means that write still sees an animated style — ours — and
|
|
114
|
+
// re-registers the pair instead.
|
|
115
|
+
const behaviorStyles = new WeakMap();
|
|
116
|
+
// The gate, matching `anyBinding`: one boolean read on the style branch of every prop write in an
|
|
117
|
+
// app whose behaviors own no animated layer, which is nearly all of them.
|
|
118
|
+
let anyBehaviorStyle = false;
|
|
119
|
+
function withBehaviorStyle(node, style) {
|
|
120
|
+
if (!anyBehaviorStyle)
|
|
121
|
+
return style;
|
|
122
|
+
const layer = behaviorStyles.get(node);
|
|
123
|
+
return layer === undefined ? style : [style, layer];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Give a node a behavior-owned animated style layer, or drop it by passing `undefined`.
|
|
127
|
+
*
|
|
128
|
+
* The seam `<touchable-opacity>` needs: RN runs its press fade from an `Animated.View` whose
|
|
129
|
+
* style is `[props.style, {opacity: anim}]` (TouchableOpacity.js:302), and a tag has no such
|
|
130
|
+
* wrapper. The layer is bound to the leaf, never folded into `node.props.style` — so the behavior
|
|
131
|
+
* can still read the AUTHOR's resting opacity back without seeing its own fade.
|
|
132
|
+
*/
|
|
133
|
+
export function setAnimatedBehaviorStyle(node, style) {
|
|
134
|
+
if (style === undefined)
|
|
135
|
+
behaviorStyles.delete(node);
|
|
136
|
+
else {
|
|
137
|
+
behaviorStyles.set(node, style);
|
|
138
|
+
anyBehaviorStyle = true;
|
|
139
|
+
}
|
|
140
|
+
// Re-register against the style standing right now, so the pair the leaf holds is always
|
|
141
|
+
// (author, layer) whichever of the two moved last.
|
|
142
|
+
bindAnimatedValue(node, 'style', propOf(node, 'style'));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resolve an animated value in a prop, returning what should be published for it.
|
|
146
|
+
*
|
|
147
|
+
* Returns its input by IDENTITY when the value holds nothing animated, so `routeProp` can call it
|
|
148
|
+
* unconditionally and every downstream branch — class, style, activeStyle, `on*`, `setProp` —
|
|
149
|
+
* keeps seeing a plain value and needs no change.
|
|
150
|
+
*/
|
|
151
|
+
export function bindAnimatedValue(node, key, value) {
|
|
152
|
+
const existing = bindings.get(node);
|
|
153
|
+
const bound = key === 'style' ? withBehaviorStyle(node, value) : value;
|
|
154
|
+
const isAnimated = bound instanceof AnimatedNode ||
|
|
155
|
+
(key === 'style' && styleHoldsAnimated(bound));
|
|
156
|
+
if (!isAnimated) {
|
|
157
|
+
// A prop that USED to be animated and no longer is: drop it, or the leaf keeps writing a
|
|
158
|
+
// stale value over whatever the app just wrote.
|
|
159
|
+
if (existing !== undefined && Object.hasOwn(existing.raw, key)) {
|
|
160
|
+
delete existing.raw[key];
|
|
161
|
+
reconcile(node, existing);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
const binding = existing ?? {
|
|
166
|
+
raw: {},
|
|
167
|
+
lifecycle: createAnimatedLeafLifecycle('host'),
|
|
168
|
+
};
|
|
169
|
+
if (existing === undefined) {
|
|
170
|
+
bindings.set(node, binding);
|
|
171
|
+
anyBinding = true;
|
|
172
|
+
}
|
|
173
|
+
binding.raw[key] = bound;
|
|
174
|
+
// Fabric flattens a view whose props do not require one, and a flattened view has no tag for
|
|
175
|
+
// the native driver to bind to. RN forces the same flag from `reduceAnimatedProps`.
|
|
176
|
+
setProp(node, 'collapsable', false);
|
|
177
|
+
reconcile(node, binding);
|
|
178
|
+
// The AUTHOR's value, rasterized — never the composed one. A behavior's layer reaches Fabric
|
|
179
|
+
// through the per-frame `setNativeProps` merge, and folding it in here would publish the fade's
|
|
180
|
+
// own output as the node's declarative style, which the behavior then reads back as resting.
|
|
181
|
+
const own = rasterize(key, value);
|
|
182
|
+
return own === undefined ? value : own;
|
|
183
|
+
}
|
|
184
|
+
const eventBindings = new WeakMap();
|
|
185
|
+
/**
|
|
186
|
+
* Bind a native-driven `Animated.event` handler written as an `on*` prop. A no-op for every other
|
|
187
|
+
* handler, so `routeProp` calls it for any `on*` name behind the same one-boolean gate.
|
|
188
|
+
*/
|
|
189
|
+
export function bindAnimatedEvent(node, propName, handler) {
|
|
190
|
+
const bound = eventBindings.get(node);
|
|
191
|
+
if (bound !== undefined) {
|
|
192
|
+
const current = bound.get(propName);
|
|
193
|
+
if (current !== undefined) {
|
|
194
|
+
// A framework hands a fresh closure most renders; only a different handler is worth a
|
|
195
|
+
// detach/attach round trip through the native module.
|
|
196
|
+
if (current.handler === handler)
|
|
197
|
+
return;
|
|
198
|
+
current.attachment.detach();
|
|
199
|
+
bound.delete(propName);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Returns undefined unless this really is a native-driven AnimatedEvent, and defers itself
|
|
203
|
+
// through `whenCommitted` until the node has a Fabric tag — no second deferral invented here.
|
|
204
|
+
const attachment = attachNativeEventHandler(node, propName, handler);
|
|
205
|
+
if (attachment === undefined)
|
|
206
|
+
return;
|
|
207
|
+
const map = bound ?? new Map();
|
|
208
|
+
if (bound === undefined)
|
|
209
|
+
eventBindings.set(node, map);
|
|
210
|
+
map.set(propName, { handler, attachment });
|
|
211
|
+
// The same gate the props half raises: teardown and re-arm are both behind it.
|
|
212
|
+
anyBinding = true;
|
|
213
|
+
}
|
|
214
|
+
// Re-attach against the tag the node has NOW. The old handles are spent — each detached against
|
|
215
|
+
// the tag it attached with, which is the point — so re-arming is a fresh attach, not a resume, and
|
|
216
|
+
// it goes back through `bindAnimatedEvent` rather than growing a second attach path. Only a node
|
|
217
|
+
// the sweep parked reaches here, and parking always detaches, so clearing without detaching first
|
|
218
|
+
// drops nothing live.
|
|
219
|
+
function reattachAnimatedEvents(node) {
|
|
220
|
+
const bound = eventBindings.get(node);
|
|
221
|
+
if (bound === undefined)
|
|
222
|
+
return;
|
|
223
|
+
const spent = [...bound];
|
|
224
|
+
bound.clear();
|
|
225
|
+
for (const [propName, binding] of spent)
|
|
226
|
+
bindAnimatedEvent(node, propName, binding.handler);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Release a node's animated subscription. Called per node from the commit sweep, which is where a
|
|
230
|
+
* genuine removal is first distinguishable from a framework spelling a move as remove-then-insert
|
|
231
|
+
* (host-behavior.ts, `markDetachCandidate`).
|
|
232
|
+
*/
|
|
233
|
+
export function detachAnimatedProps(node) {
|
|
234
|
+
// The sweep runs for host behaviors too, so an app that animates nothing must not pay a WeakSet
|
|
235
|
+
// insert per removed node to learn it has nothing to release.
|
|
236
|
+
if (!anyBinding)
|
|
237
|
+
return;
|
|
238
|
+
parked.add(node);
|
|
239
|
+
bindings.get(node)?.lifecycle.teardown();
|
|
240
|
+
const bound = eventBindings.get(node);
|
|
241
|
+
if (bound === undefined)
|
|
242
|
+
return;
|
|
243
|
+
for (const binding of bound.values())
|
|
244
|
+
binding.attachment.detach();
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Re-arm a subtree the sweep tore down and the framework put back — Svelte parks LIVE nodes
|
|
248
|
+
* offscreen across commits and returns them with their props unwritten, so without this the
|
|
249
|
+
* animation stops with nothing red anywhere.
|
|
250
|
+
*
|
|
251
|
+
* A WeakSet miss and no walk at all for every node in a freshly built tree, which is the path that
|
|
252
|
+
* runs ~9 000 times per benchmark create.
|
|
253
|
+
*/
|
|
254
|
+
export function reattachAnimatedProps(node) {
|
|
255
|
+
if (!parked.has(node))
|
|
256
|
+
return;
|
|
257
|
+
// FLAT, one host read for the whole subtree — the twin of `reattachSubtree` in `host-behavior.ts`,
|
|
258
|
+
// which was converted when the teardown sweep turned out to be paying a JSI crossing per node.
|
|
259
|
+
// This one was missed then. It is reached far less often (the guard above is a WeakSet miss for
|
|
260
|
+
// every node of a freshly built tree) but costs the same per node when it does run, and a parked
|
|
261
|
+
// subtree is a whole screen's worth in the case it exists for — Svelte parking live nodes
|
|
262
|
+
// offscreen across commits.
|
|
263
|
+
//
|
|
264
|
+
// The recursion stopped at an unparked node where this skips it and carries on; the two agree
|
|
265
|
+
// because `parked` is filled by `detachAnimatedProps` over a whole detached subtree, so a node in
|
|
266
|
+
// it has its descendants in it.
|
|
267
|
+
for (const each of subtreesOf([node]))
|
|
268
|
+
reattachOneAnimated(each);
|
|
269
|
+
}
|
|
270
|
+
function reattachOneAnimated(node) {
|
|
271
|
+
if (!parked.has(node))
|
|
272
|
+
return;
|
|
273
|
+
parked.delete(node);
|
|
274
|
+
const binding = bindings.get(node);
|
|
275
|
+
if (binding !== undefined)
|
|
276
|
+
reconcile(node, binding);
|
|
277
|
+
reattachAnimatedEvents(node);
|
|
278
|
+
}
|
|
@@ -4,7 +4,7 @@ export { AnimatedValueXY, type IValueXY } from './value-xy';
|
|
|
4
4
|
export { AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, add, subtract, multiply, divide, modulo, diffClamp, } from './operators';
|
|
5
5
|
export { AnimatedEvent, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, type IEventConfig, type IEventListener, type IAnimatedEventHandler, type INativeEventAttachment, } from './event';
|
|
6
6
|
export { AnimatedColor, type IRgbaValue, type IColorInput } from './color';
|
|
7
|
-
export { Easing, type IEasingFunction } from './easing';
|
|
7
|
+
export { Easing, type IEasing, type IEasingFunction } from './easing';
|
|
8
8
|
export { createNumericInterpolation, createInterpolation, checkValidRanges, type IInterpolationConfig, type IExtrapolateType, } from './interpolation';
|
|
9
9
|
export type { IAnimation, IEndCallback, IEndResult } from './animation';
|
|
10
10
|
export { TimingAnimation } from './animations/timing';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
|
3
|
-
//
|
|
2
|
+
// props, swap it into the value graph, bind it to the committed node, and go native when asked.
|
|
3
|
+
// Framework-agnostic on purpose - it knows nothing about hooks,
|
|
4
4
|
// effects, change detection or reactivity, only the engine's own Animated primitives.
|
|
5
5
|
//
|
|
6
6
|
// WHY THIS LIVES IN THE ENGINE. It used to live four times, once per adapter (React's
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
// What stays in the adapter: WHEN to call reconcile (an effect, onUpdated, ngOnChanges, a $effect)
|
|
16
16
|
// and HOW to find the host node (a ref, a ViewChild, a shim). Everything below is the same for all.
|
|
17
17
|
import { AnimatedProps } from './props.js';
|
|
18
|
-
import { attachNativeEventHandler } from './event.js';
|
|
19
18
|
import { dlog } from '../debug.js';
|
|
20
19
|
// Diagnostic-only, DEBUG-gated: a process-wide sequence number so every reconcile call across
|
|
21
20
|
// every Animated.* instance in a log dump is individually identifiable and orderable.
|
|
@@ -66,7 +65,6 @@ function describeTransform(value) {
|
|
|
66
65
|
}
|
|
67
66
|
export function createAnimatedLeafLifecycle(label) {
|
|
68
67
|
let attached = null;
|
|
69
|
-
let eventDetachers = [];
|
|
70
68
|
// Diagnostic-only: the last node identity, plus a reentrancy flag to catch reconcile() being
|
|
71
69
|
// called AGAIN from inside its own call stack - the smoking-gun shape for a synchronous
|
|
72
70
|
// same-flush loop, as opposed to merely "called often".
|
|
@@ -77,11 +75,6 @@ export function createAnimatedLeafLifecycle(label) {
|
|
|
77
75
|
let lastWantsNative = false;
|
|
78
76
|
// Canceller for a native bind the caller deferred and that has not run yet.
|
|
79
77
|
let cancelPendingBind;
|
|
80
|
-
function detachEvents() {
|
|
81
|
-
for (const detach of eventDetachers)
|
|
82
|
-
detach();
|
|
83
|
-
eventDetachers = [];
|
|
84
|
-
}
|
|
85
78
|
return {
|
|
86
79
|
reconcile(props, node, wantsNative, scheduleNativeBind) {
|
|
87
80
|
const seq = ++globalReconcileSeq;
|
|
@@ -142,23 +135,19 @@ export function createAnimatedLeafLifecycle(label) {
|
|
|
142
135
|
if (attached !== null && attached !== newLeaf)
|
|
143
136
|
attached.__detach();
|
|
144
137
|
attached = newLeaf;
|
|
145
|
-
// The native half, which a caller may defer.
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
138
|
+
// The native half, which a caller may defer.
|
|
139
|
+
//
|
|
140
|
+
// It used to native-attach every `Animated.event` prop here as well. It must not any more:
|
|
141
|
+
// a wrapper hands those same props DOWN to a host element, so they reach `routeProp`,
|
|
142
|
+
// which binds them itself (`host-binding.ts`, `bindAnimatedEvent`) — that is what makes a
|
|
143
|
+
// bare `<scroll-view onScroll={…} />` work with no wrapper at all. Doing it here too
|
|
144
|
+
// registered the SAME mapping twice on one view tag, caught by the Vue and Svelte
|
|
145
|
+
// wrapper tests the day the engine half landed.
|
|
149
146
|
const bindNative = () => {
|
|
150
147
|
if (node !== null)
|
|
151
148
|
newLeaf.setNativeView(node);
|
|
152
149
|
if (wantsNative)
|
|
153
150
|
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
151
|
};
|
|
163
152
|
cancelPendingBind?.();
|
|
164
153
|
cancelPendingBind = undefined;
|
|
@@ -175,7 +164,6 @@ export function createAnimatedLeafLifecycle(label) {
|
|
|
175
164
|
teardown() {
|
|
176
165
|
cancelPendingBind?.();
|
|
177
166
|
cancelPendingBind = undefined;
|
|
178
|
-
detachEvents();
|
|
179
167
|
if (attached !== null) {
|
|
180
168
|
attached.__detach();
|
|
181
169
|
attached = null;
|
package/build/animated/mock.d.ts
CHANGED
|
@@ -16,25 +16,7 @@ export declare const AnimatedMock: {
|
|
|
16
16
|
Value: typeof AnimatedValue;
|
|
17
17
|
ValueXY: typeof AnimatedValueXY;
|
|
18
18
|
Color: typeof AnimatedColor;
|
|
19
|
-
Easing:
|
|
20
|
-
step0(n: number): number;
|
|
21
|
-
step1(n: number): number;
|
|
22
|
-
linear(t: number): number;
|
|
23
|
-
ease(t: number): number;
|
|
24
|
-
quad(t: number): number;
|
|
25
|
-
cubic(t: number): number;
|
|
26
|
-
poly(n: number): import("./easing").IEasingFunction;
|
|
27
|
-
sin(t: number): number;
|
|
28
|
-
circle(t: number): number;
|
|
29
|
-
exp(t: number): number;
|
|
30
|
-
elastic(bounciness?: number): import("./easing").IEasingFunction;
|
|
31
|
-
back(s?: number): import("./easing").IEasingFunction;
|
|
32
|
-
bounce(t: number): number;
|
|
33
|
-
bezier(x1: number, y1: number, x2: number, y2: number): import("./easing").IEasingFunction;
|
|
34
|
-
in(easing: import("./easing").IEasingFunction): import("./easing").IEasingFunction;
|
|
35
|
-
out(easing: import("./easing").IEasingFunction): import("./easing").IEasingFunction;
|
|
36
|
-
inOut(easing: import("./easing").IEasingFunction): import("./easing").IEasingFunction;
|
|
37
|
-
};
|
|
19
|
+
Easing: import("./easing").IEasing;
|
|
38
20
|
timing: typeof timing;
|
|
39
21
|
spring: typeof spring;
|
|
40
22
|
decay: typeof decay;
|
package/build/animated/props.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// a `update` function off the node, and under useDefineForClassFields a field
|
|
13
13
|
// initialiser would shadow it and break subclassing. (See shared graph.ts leafUpdate.)
|
|
14
14
|
import { AnimatedNode, AnimatedWithChildren } from './graph.js';
|
|
15
|
-
import { setNativeProps, getNativeTag } from '../
|
|
15
|
+
import { setNativeProps, getNativeTag } from '../imperative.js';
|
|
16
16
|
import { isSymbioteNode } from '../node.js';
|
|
17
17
|
import { registerPostCommit } from '../post-commit.js';
|
|
18
18
|
import { nativeAnimated, } from './native/native-animated.js';
|