@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.
Files changed (96) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -7
  10. package/build/accessibility-props.js +22 -21
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/graph.d.ts +2 -0
  17. package/build/animated/graph.js +14 -0
  18. package/build/animated/host-binding.d.ts +39 -0
  19. package/build/animated/host-binding.js +278 -0
  20. package/build/animated/index.d.ts +1 -1
  21. package/build/animated/leaf-lifecycle.js +10 -22
  22. package/build/animated/mock.d.ts +1 -19
  23. package/build/animated/props.js +1 -1
  24. package/build/animated/rgba.js +16 -50
  25. package/build/events/index.js +123 -33
  26. package/build/fabric-props.d.ts +1 -1
  27. package/build/fabric-props.js +129 -182
  28. package/build/fabric.d.ts +10 -0
  29. package/build/fabric.js +40 -0
  30. package/build/host-access.d.ts +125 -0
  31. package/build/host-access.js +280 -0
  32. package/build/host-behavior.d.ts +107 -7
  33. package/build/host-behavior.js +309 -31
  34. package/build/image-source-write.d.ts +16 -0
  35. package/build/image-source-write.js +65 -0
  36. package/build/imperative.d.ts +49 -0
  37. package/build/imperative.js +258 -0
  38. package/build/index.d.ts +18 -10
  39. package/build/index.js +65 -10
  40. package/build/mutation-buffer.d.ts +222 -0
  41. package/build/mutation-buffer.js +491 -0
  42. package/build/native-engine.d.ts +182 -0
  43. package/build/native-engine.js +178 -0
  44. package/build/native-tree-host.d.ts +25 -0
  45. package/build/native-tree-host.js +66 -0
  46. package/build/node.d.ts +191 -60
  47. package/build/node.js +1034 -327
  48. package/build/pan-responder/index.d.ts +2 -2
  49. package/build/pan-responder/index.js +37 -56
  50. package/build/platform-color/index.d.ts +1 -1
  51. package/build/platform-color/index.js +11 -4
  52. package/build/process-background-image/index.js +30 -566
  53. package/build/process-background-longhands.d.ts +4 -0
  54. package/build/process-background-longhands.js +44 -0
  55. package/build/process-box-shadow/index.js +23 -187
  56. package/build/process-filter.js +27 -300
  57. package/build/process-transform/index.d.ts +1 -1
  58. package/build/process-transform/index.js +25 -107
  59. package/build/process-transform-origin/index.d.ts +1 -1
  60. package/build/process-transform-origin/index.js +29 -102
  61. package/build/registry.d.ts +36 -0
  62. package/build/registry.js +73 -0
  63. package/build/sound-manager/index.d.ts +3 -0
  64. package/build/sound-manager/index.js +36 -0
  65. package/build/structured-style.d.ts +10 -0
  66. package/build/structured-style.js +180 -0
  67. package/build/style-registry/index.d.ts +14 -0
  68. package/build/style-registry/index.js +60 -11
  69. package/build/styles.d.ts +5 -1
  70. package/build/surface.d.ts +31 -2
  71. package/build/surface.js +138 -42
  72. package/build/text-input-state.d.ts +1 -0
  73. package/build/text-input-state.js +17 -3
  74. package/build/tree-host.d.ts +307 -0
  75. package/build/tree-host.js +211 -0
  76. package/build/view-config.js +4 -4
  77. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  78. package/cpp/SymbioteDebug.cpp +51 -0
  79. package/cpp/SymbioteDebug.h +54 -0
  80. package/cpp/SymbioteEngineBindings.cpp +232 -0
  81. package/cpp/SymbioteEngineBindings.h +59 -0
  82. package/cpp/SymbioteFabricProps.cpp +2619 -0
  83. package/cpp/SymbioteFabricProps.h +223 -0
  84. package/cpp/SymbioteTree.cpp +2478 -0
  85. package/cpp/SymbioteTree.h +257 -0
  86. package/ios/SymbioteEngineModule.h +25 -0
  87. package/ios/SymbioteEngineModule.mm +44 -0
  88. package/package.json +31 -3
  89. package/react-native.config.cjs +23 -0
  90. package/symbiote-engine.podspec +42 -0
  91. package/build/animated/bezier.d.ts +0 -1
  92. package/build/animated/bezier.js +0 -102
  93. package/build/commit.d.ts +0 -49
  94. package/build/commit.js +0 -1030
  95. package/build/tags.d.ts +0 -2
  96. package/build/tags.js +0 -40
@@ -1,88 +1,17 @@
1
- // Common easing functions, ported from React Native's Libraries/Animated/Easing.js.
2
- // Pure math (the only dependency is the equally-pure bezier solver), so it lives
3
- // unchanged in shared and every adapter re-exports it.
4
- import { bezier } from './bezier.js';
5
- let cachedEase;
6
- export const Easing = {
7
- step0(n) {
8
- return n > 0 ? 1 : 0;
9
- },
10
- step1(n) {
11
- return n >= 1 ? 1 : 0;
12
- },
13
- linear(t) {
14
- return t;
15
- },
16
- // A simple inertial interaction (object slowly accelerating to speed).
17
- ease(t) {
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;
@@ -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 '../commit.js';
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;
@@ -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, and
3
- // rebind native event props. Framework-agnostic on purpose - it knows nothing about hooks,
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. 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.
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;
@@ -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;
@@ -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 '../commit.js';
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';