react-voodoo 2.6.1 → 2.6.3

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 CHANGED
@@ -1,274 +1,370 @@
1
- # react-voodoo
1
+ <h1 align="center">react-voodoo</h1>
2
+ <p align="center"><b>Additive · Swipeable · SSR-ready · Physics-based</b><br/>A delta-driven tween composition engine for React</p>
2
3
 
3
- Additive, swipeable, SSR-ready animation engine for React.
4
+ <p align="center"><img width="192" src="https://github.com/react-voodoo/react-voodoo/raw/master/doc/assets/logo-v0.png?sanitize=true" /></p>
4
5
 
5
- [![npm](https://img.shields.io/npm/v/react-voodoo)](https://www.npmjs.com/package/react-voodoo)
6
- [![license](https://img.shields.io/badge/license-CC--BY--ND--4.0%20OR%20AGPL--3.0-blue)](#license)
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/react-voodoo"><img src="https://img.shields.io/npm/v/react-voodoo.svg" alt="npm version" /></a>
8
+ <a href="https://travis-ci.org/react-voodoo/react-voodoo"><img src="https://travis-ci.org/react-voodoo/react-voodoo.svg?branch=master" alt="Build Status" /></a>
9
+ <img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat" />
10
+ <br/>
11
+ <a href="http://creativecommons.org/licenses/by-nd/4.0"><img src="https://img.shields.io/badge/License-CC%20BY--ND%204.0-lightgrey.svg" alt="License: CC BY-ND 4.0" /></a>
12
+ <a href="http://www.gnu.org/licenses/agpl-3.0"><img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3" /></a>
13
+ </p>
7
14
 
8
- ## Overview
15
+ <p align="center">
16
+ <a href="doc/readme.md"><b>Full documentation</b></a> &nbsp;·&nbsp;
17
+ <a href="https://react-voodoo.github.io/react-voodoo-samples/"><b>Live demos & CodeSandbox</b></a> &nbsp;·&nbsp;
18
+ <a href="https://github.com/react-voodoo/react-voodoo-samples"><b>Sample sources</b></a>
19
+ </p>
9
20
 
10
- react-voodoo drives animations by writing CSS **directly to the DOM**, bypassing React's render loop entirely. Scroll positions, drag gestures, and programmatic tweens all feed into the same additive accumulator, so multiple concurrent animations compose without conflict.
11
-
12
- The engine is built on [tween-axis](../tween-axis/README.md) and uses its WebAssembly backend for hot-path property accumulation with zero JS-boundary crossings per frame.
21
+ <p align="center"><img src="https://github.com/react-voodoo/react-voodoo/raw/master/doc/assets/demo.gif?sanitize=true" /></p>
13
22
 
14
23
  ---
15
24
 
16
- ## Installation
25
+ ## Why react-voodoo?
17
26
 
18
- ```bash
19
- npm install react-voodoo
20
- ```
21
-
22
- Requires React 16, 17, or 18.
23
-
24
- ---
27
+ Most animation libraries output **absolute values** — they own a CSS property and write a number to it each frame. That works fine for isolated transitions, but breaks down the moment you need to combine sources: a scroll position driving `translateY`, a drag gesture adding to the same `translateY`, and a parallax offset stacking on top. The libraries fight each other and you end up writing glue code.
25
28
 
26
- ## Core concepts
29
+ React-voodoo takes a different approach: its engine computes **deltas** — the *change* from the previous frame — and accumulates them additively across any number of axes. Multiple animations on the same property simply add together. No ownership, no conflicts.
27
30
 
28
- ### Delta-based accumulation
29
-
30
- Every animation operates on **deltas** — the change from the previous timeline position — rather than absolute target values. On each scroll or animation frame, `goTo(newPos, tweenRefMaps)` emits deltas that are added into a numeric accumulator (`tweenRefMaps`). A separate mux pass then converts those numbers into CSS strings and writes them to `node.style` directly.
31
-
32
- This architecture enables:
33
- - Multiple axes and one-shot animations to animate the same property simultaneously (additive composition).
34
- - Direct DOM writes that skip React's reconciler entirely.
35
- - Predictive inertia and physics-based scrolling with no render overhead.
36
-
37
- ### CSS demux / mux
38
-
39
- Tween descriptors specify CSS properties in their `apply` object. Before registering with `TweenAxis`, the `deMuxLine` pass converts each CSS value into a set of flat numeric keys (e.g. `transform_0_translateX_9`). On every frame the inverse `muxToCss` pass reconstructs the CSS strings (including `calc()` for multi-unit values) and writes them to the DOM.
40
-
41
- ### WASM acceleration
31
+ The engine is built on [tween-axis](../tween-axis/README.md) and uses its WebAssembly backend for hot-path property accumulation with zero JS-boundary crossings per frame.
42
32
 
43
- For tween descriptors without event callbacks and with a standard easing function, `CssTweenAxis` (the internal axis class) automatically switches the process to `PROC_WASM` mode. The WebAssembly engine accumulates the property deltas directly inside WASM — no JS function call per property per frame. Results are flushed back into `tweenRefMaps` in a tight loop after `goTo()` returns.
33
+ This unlocks a set of features that are unique to the delta model:
44
34
 
45
- Descriptors that use custom easing functions or event callbacks (`entering`/`moving`/`leaving`) are transparently handled by the JS path (`PROC_RESULT`), so there is no behavioural change — WASM acceleration applies automatically where possible.
35
+ | Feature | How |
36
+ |---|---|
37
+ | **Additive multi-axis composition** | Each axis contributes a delta; they stack without coordination code. |
38
+ | **Swipeable / draggable animations** | Drag gestures are mapped directly to axis positions with realistic momentum. |
39
+ | **Predictive inertia** | The engine computes the final snap target *at the moment of release*, before the animation plays out — useful for preloading the next slide. |
40
+ | **SSR with correct initial styles** | Axes have a `defaultPosition`; styles are computed server-side and rendered inline — no flash on first paint. |
41
+ | **DOM writes bypass React** | Style updates go straight to `node.style` via direct DOM writes, never triggering a re-render. |
42
+ | **Multi-unit CSS via `calc()`** | Mix `%`, `px`, `vw`, `bw`/`bh` (box-relative units) in a single value — compiled to `calc()` automatically. |
46
43
 
47
44
  ---
48
45
 
49
- ## Components
50
-
51
- ### `<Component>` / `asTweener`
52
-
53
- The root animation engine. Wrap your animated tree with it to create a `Tweener` context.
46
+ ## Comparison
47
+
48
+ ### Feature matrix
49
+
50
+ | | **react-voodoo** | Framer Motion v12 | GSAP + ScrollTrigger | react-spring v10 | Motion v5 | anime.js v4 |
51
+ |---|:---:|:---:|:---:|:---:|:---:|:---:|
52
+ | Scroll-linked animation | ✅ | ✅ `useScroll` | ✅ | ⚠️ manual | ✅ | ✅ `ScrollObserver` |
53
+ | Drag-linked animation | ✅ native | ✅ `drag` | ⚠️ manual | ✅ `@use-gesture` | ✅ gestures | ✅ `Draggable` |
54
+ | **Additive multi-axis composition** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
55
+ | Physics / momentum inertia | ✅ predictive | ✅ spring | ❌ | ✅ spring | ⚠️ limited | ✅ spring |
56
+ | **Predictive snap target** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
57
+ | **SSR — correct initial styles** | ✅ | ⚠️ flash | ⚠️ flash | ⚠️ flash | ⚠️ flash | ❌ |
58
+ | Bypasses React render loop | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
59
+ | Transform layer composition | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
60
+ | SVG geometry attributes | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
61
+ | Multitouch (drag multiple axes) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
62
+ | Bundle size (approx. gzip) | ~18 kB | ~32 kB | ~35 kB | ~25 kB | ~4 kB | ~10 kB |
63
+ | React dependency | ≥ 16 | ≥ 18 | none | ≥ 16 | none | none |
54
64
 
55
- ```jsx
56
- import { Component } from "react-voodoo";
57
- // or with the HOC:
58
- import { tweener as asTweener } from "react-voodoo";
65
+ ---
59
66
 
60
- @asTweener
61
- class MyScene extends React.Component { ... }
62
- ```
67
+ ## Performance
63
68
 
64
- ### `<Node>`
69
+ The delta model isn't just an architectural choice — the numbers back it up.
70
+ In the scenarios that define react-voodoo's use-case — **compositing multiple animation
71
+ sources on the same element** — the engine runs **3–7× faster than GSAP** and handles
72
+ far more properties per frame without degrading.
65
73
 
66
- An animatable React element. Registers its CSS tween descriptors with the parent `Tweener`.
74
+ <p align="center">
75
+ <img src="./doc/assets/perf-chart.svg" alt="Performance comparison chart" width="960"/>
76
+ </p>
67
77
 
68
- ```jsx
69
- import { Node } from "react-voodoo";
78
+ <details>
79
+ <summary><b>What each scenario measures</b></summary>
70
80
 
71
- <Node
72
- id="card"
73
- style={{ position: "absolute" }}
74
- tweenLines={[
75
- {
76
- from: 0, duration: 500,
77
- apply: { opacity: 1, transform: [{ translateY: "100px" }] },
78
- easeFn: "easeQuadInOut",
79
- }
80
- ]}
81
- />
82
- ```
81
+ | Scenario | Description |
82
+ |---|---|
83
+ | **sequential · 5 props** | Frame-by-frame advance, 5 CSS properties — the baseline everyone tests |
84
+ | **property scale · 20 props** | Same advance with 20 properties — reveals engine scaling cost per property |
85
+ | **additive ×3** | Three independent animation axes all writing to the same 5 properties simultaneously. This is react-voodoo's **native model**. Competitors must run 3 separate timelines and manually sum results. |
86
+ | **spring layers** | Same 3 axes at different speeds (×1, ×0.7, ×0.3) — the signature scroll + drag + push composition pattern react-voodoo is built for |
87
+ | **long timeline · 20 segs** | A timeline with 20 sequential animation segments — models a full-page scroll sequence or complex keyframe chain |
83
88
 
84
- ### `<Axis>`
89
+ </details>
85
90
 
86
- A scrollable animation timeline. Attach it to a scroll axis by `id`.
91
+ <details>
92
+ <summary><b>Why voodoo wins at scale</b></summary>
87
93
 
88
- ```jsx
89
- import { Axis } from "react-voodoo";
90
-
91
- <Axis
92
- id="scrollY"
93
- size={1000}
94
- defaultPosition={0}
95
- inertia={{ snapToBounds: true, wayPoints: [{ at: 0 }, { at: 500 }] }}
96
- />
97
- ```
94
+ - **Compiled-per-property processors** — at mount time each tween compiles a dedicated function (via `new Function`) that contains only the branches it needs. There is no per-property dispatch loop at runtime; property count scales at near O(1).
95
+ - **WASM state machine** the marker-scan loop (the part that decides which tweens are active as the cursor moves) runs entirely in WebAssembly. Zero JS-boundary crossings per frame in the hot path.
96
+ - **Additive is free** — `goTo(pos, scope)` accumulates deltas into the same plain object regardless of how many axes call it. Competitors must maintain N separate target objects and merge them manually.
98
97
 
99
- ### `<Draggable>`
98
+ The one scenario where a stateless interpolator (Framer Motion / Popmotion) wins is
99
+ a **single small axis with easing**: pure math functions with no timeline state beat even
100
+ WASM for trivially short timelines. That overhead is the fair cost of additive composability —
101
+ and it disappears entirely once you have more than one axis compositing on the same element.
100
102
 
101
- Maps touch/mouse drag gestures to an axis position.
103
+ </details>
102
104
 
103
- ```jsx
104
- import { Draggable } from "react-voodoo";
105
-
106
- <Draggable axisId="scrollY" />
107
- ```
105
+ > Full benchmark source: [`perf-compare/bench.js`](../perf-compare/bench.js)
106
+ > Detailed analysis: [`doc/Alternatives libs perf comparaison.md`](doc/Alternatives%20libs%20perf%20comparaison.md)
108
107
 
109
108
  ---
110
109
 
111
- ## Hooks
112
-
113
- ### `useVoodoo(opts?)`
114
-
115
- Primary hook. Creates or inherits a `Tweener` instance and returns `[tweener, ViewBox]`.
116
-
117
- ```jsx
118
- import { useVoodoo } from "react-voodoo";
110
+ ## Installation
119
111
 
120
- function Scene() {
121
- const [tweener, ViewBox] = useVoodoo();
122
- return <ViewBox>{...}</ViewBox>;
123
- }
112
+ ```bash
113
+ npm install react-voodoo
124
114
  ```
125
115
 
126
- ### `useTweener()`
127
-
128
- Read-only access to the nearest parent `Tweener`.
129
-
130
- ```jsx
131
- import { useTweener } from "react-voodoo";
132
-
133
- function Inner() {
134
- const tweener = useTweener();
135
- // tweener.scrollTo("scrollY", 500, 300);
136
- }
137
- ```
116
+ **Peer dependencies:** `react >= 16`, `react-dom >= 16`
138
117
 
139
118
  ---
140
119
 
141
- ## Tween descriptor
142
-
143
- ```js
144
- {
145
- from: 0, // Start position on the axis (omit for sequential chaining)
146
- duration: 500, // Length of this segment
147
- target: "nodeId", // ID of the Node to animate
148
- apply: {
149
- opacity: 1,
150
- transform: [{ translateX: "100px", translateY: "-50%" }],
151
- // Multi-unit values use CSS calc():
152
- width: ["50%", "10vw", "-50px"],
153
- },
154
- easeFn: "easeQuadInOut", // String key into TweenAxis.EasingFunctions (d3-ease)
155
- entering: (delta) => {}, // Called when the process activates
156
- moving: (pos, prev, delta) => {},
157
- leaving: (delta) => {},
120
+ ## All-in-one example
121
+
122
+ Every major feature in a single component, with comments explaining each part.
123
+
124
+ ```jsx harmony
125
+ import React from "react";
126
+ import Voodoo from "react-voodoo";
127
+ import {itemTweenAxis, tweenArrayWithTargets} from "./somewhere";
128
+
129
+ const styleSample = {
130
+ /**
131
+ * Voodoo.Node style property and the tween descriptors use classic CSS-in-JS declaration
132
+ * exept we can specify values using the "box" unit which is a [0-1] ratio of the parent ViewBox height / width
133
+ */
134
+
135
+ height: "50%",
136
+
137
+ // the tweener deal with multiple units
138
+ // it will use css calc fn to add them if there's more than 1 unit used
139
+ width: ["50%", "10vw", "-50px", ".2box"],
140
+
141
+ // transform can use multiple "layers"
142
+ transform: [
143
+ {
144
+ // use rotate(X|Y|Z) & translate(X|Y|Z)
145
+ rotateX: "25deg"
146
+ },
147
+ {
148
+ translateZ: "-.2box"
149
+ }
150
+ ],
151
+
152
+ filter:
153
+ {
154
+ blur: "5px"
155
+ }
156
+ };
157
+ const axisSample = [// Examples of tween descriptors
158
+ {
159
+ target : "someTweenRefId", // target Voodoo.Node id ( optional if used as parameter on a Voodoo.Node as it will target it )
160
+ from : 0, // tween start position
161
+ duration: 100, // tween duration
162
+ easeFn : "easeCircleIn", // function or easing fn id from [d3-ease](https://github.com/d3/d3-ease)
163
+
164
+ apply: {// relative css values to be applied
165
+ // Same syntax as the styles
166
+ transform: [{}, {
167
+ translateZ: "-.2box"
168
+ }]
169
+ }
170
+ },
171
+ {
172
+ from : 40,
173
+ duration: 20,
174
+
175
+ // triggered when axis has scrolled in the Event period
176
+ // delta : a float value between [-1,1] is the update inside the Event period
177
+ entering: ( delta ) => false,
178
+
179
+ // triggered when axis has scrolled in the Event period
180
+ // newPos, precPos : float values between [0,1] position inside the Event period
181
+ // delta : a float value between [-1,1] is the update inside the Event period
182
+ moving: ( newPos, precPos, delta ) => false,
183
+
184
+ // triggered when axis has scrolled out the Event period
185
+ // delta : a float value between [-1,1] is the update inside the Event period
186
+ leaving: ( delta ) => false
187
+ }
188
+ ];
189
+
190
+ const Sample = ( {} ) => {
191
+
192
+ /**
193
+ * Voodoo tweener instanciation
194
+ */
195
+ // Classic minimal method
196
+ const [tweener, ViewBox] = Voodoo.hook();
197
+ // get the first tweener in parents
198
+ const [parentTweener] = Voodoo.hook(true);
199
+ // Create a tweener with options
200
+ const [twenerWithNameAndOptions, ViewBox2] = Voodoo.hook(
201
+ {
202
+ // Give an id to this tweener so we can access it's axes in the childs components
203
+ name: "root",
204
+ // max click tm in ms before a click become a drag
205
+ maxClickTm: 200,
206
+ // max drag offset in px before a click become a drag
207
+ maxClickOffset: 100,
208
+ // lock to only 1 drag direction
209
+ dragDirectionLock: false,
210
+ // allow dragging with mouse
211
+ enableMouseDrag: false
212
+ }
213
+ );
214
+ // get a named parent tweener
215
+ const [nammedParentTweener] = Voodoo.hook("root")
216
+
217
+ /**
218
+ * once first render done, axes expose the following values & functions :
219
+ */
220
+ // Theirs actual position in :
221
+ // tweener.axes.(axisId).scrollPos
222
+
223
+ // The "scrollTo" function allowing to manually move the axes positions :
224
+ // tweener.axes.(axisId).scrollTo(targetPos, duration, easeFn)
225
+ // tweener.scrollTo(targetPos, duration, axisId, easeFn)
226
+
227
+ // They can also be watched using the "watchAxis" function;
228
+ // When called, the returned function will disable the listener if executed :
229
+ React.useEffect(
230
+ e => tweener?.watchAxis("scrollY", ( pos ) => doSomething()),
231
+ [tweener]
232
+ )
233
+
234
+ return <ViewBox className={"container"}>
235
+ <Voodoo.Axis
236
+
237
+ id={"scrollY"} // Tween axis Id
238
+ defaultPosition={100} // optional initial position ( default : 0 )
239
+
240
+ // optional Array of tween descriptors with theirs Voodoo.Node target ids ( see axisSample )
241
+ items={tweenArrayWithTargets}
242
+
243
+ // optional size of the scrollable window for drag synchronisation
244
+ scrollableWindow={200}
245
+
246
+ // optional length of this scrollable axis (default to last tween desciptor position+duration)
247
+ size={1000}
248
+
249
+ // optional bounds ( inertia will target them if target pos is out )
250
+ bounds={{ min: 100, max: 900 }}
251
+
252
+ // optional inertia cfg ( false to disable it )
253
+ inertia={
254
+ {
255
+ // called when inertia is updated
256
+ // should return instantaneous move to do if wanted
257
+ shouldLoop: ( currentPos ) => (currentPos > 500 ? -500 : null),
258
+
259
+ // called when inertia know where it will end ( when the user stop dragging )
260
+ willEnd: ( targetPos, targetDelta, duration ) => {
261
+ },
262
+
263
+ // called when inertia know where it will snap ( when the user stop dragging )
264
+ willSnap: ( currentSnapIndex, targetWayPointObj ) => {
265
+ },
266
+
267
+ // called when inertia end
268
+ onStop: ( pos, targetWayPointObj ) => {
269
+ },
270
+
271
+ // called when inertia end on a snap
272
+ onSnap: ( snapIndex, targetWayPointObj ) => {
273
+ },
274
+
275
+ // list of waypoints object ( only support auto snap 50/50 for now )
276
+ wayPoints: [{ at: 100 }, { at: 200 }]
277
+ }
278
+ }
279
+ />
280
+
281
+ <Voodoo.Node
282
+ id={"testItem"} // optional id
283
+
284
+ style={styleSample}// optional styles applied before any style coming from axes : css syntax + voodoo tweener units & transform management
285
+
286
+ axes={{ scrollY: axisSample }} // optional Array of tween by axis Id with no target node id required ( it will be ignored )
287
+
288
+ onClick={// all unknow props are passed to the child node
289
+ ( e ) => {
290
+ // start playing an anim ( prefer scrolling Axes )
291
+ tweener.pushAnim(
292
+ // make all tween target "testItem"
293
+ Voodoo.tools.target(pushIn, "testItem")
294
+ ).then(
295
+ ( tweenAxis ) => {
296
+ // doSomething next
297
+ }
298
+ );
299
+ }
300
+ }
301
+ >
302
+ <Voodoo.Draggable
303
+ // make drag y move the scrollAnAxis axis
304
+ // xAxis={ "scrollAnAxis" }
305
+
306
+ // scale / inverse dispatched delta
307
+ // xHook={(delta)=>modify(delta)}
308
+
309
+ // React ref to the box, default to the parent ViewBox
310
+ // scale is as follow : (delta / ((xBoxRef||ViewBox).offsetWidth)) * ( axis.scrollableWindow || axis.duration )
311
+ // xBoxRef={ref}
312
+
313
+ yAxis={"scrollY"}// make drag y move the scrollY axis
314
+ // yHook={(delta)=>modify(delta)}
315
+ // yBoxRef={ref}
316
+
317
+ // mouseDrag={true} // listen for mouse drag ( default to false )
318
+ // touchDrag={false} // listen for touch drag ( default to true )
319
+
320
+ // button={1-3} // limit mouse drag to the specified event.button === ( default to 1; the left btn )
321
+
322
+ // * actually Draggable create it's own div node
323
+ >
324
+ <div>
325
+ Some content to tween
326
+ </div>
327
+ </Voodoo.Draggable>
328
+ </Voodoo.Node>
329
+ </ViewBox>;
158
330
  }
159
331
  ```
160
332
 
161
- ### Supported CSS properties
162
-
163
- All standard animatable CSS properties are supported. Complex properties have dedicated demuxers:
164
-
165
- | Property | Notes |
166
- |----------|-------|
167
- | `transform` | Array of layer objects; each key is a transform function (`translateX`, `rotate`, etc.) |
168
- | `boxShadow` | Full multi-shadow support |
169
- | `filter` | CSS filter functions |
170
- | `backgroundColor` | RGBA component interpolation |
171
- | `textShadow` | Multi-shadow support |
172
- | `opacity`, `zIndex` | Numeric |
173
- | `width`, `height`, `top`, `left`, `right`, `bottom`, margins, paddings, borders | Length with unit |
174
- | SVG attributes (`cx`, `cy`, `r`, `x`, `y`, …) | Applied via `setAttribute` |
175
-
176
- ### Multi-unit values
177
-
178
- Arrays of CSS values are resolved with `calc()`:
179
-
180
- ```js
181
- width: ["50%", "10vw", "-50px"]
182
- // → calc(50% + 10vw - 50px)
183
- ```
333
+ For a more complete annotated example with inertia callbacks, `watchAxis`, and programmatic scrolling, see the [full documentation](doc/readme.md).
184
334
 
185
335
  ---
186
336
 
187
- ## Axis configuration
188
-
189
- ```js
190
- {
191
- id: "scrollY",
192
- defaultPosition: 0,
193
- size: 1000, // Timeline length (matches tween descriptor positions)
194
- bounds: { min: 0, max: 1000 },
195
- inertia: {
196
- snapToBounds: true,
197
- wayPoints: [{ at: 0 }, { at: 500 }, { at: 1000 }],
198
- willSnap: (index, wp) => {},
199
- onSnap: (index, wp) => {},
200
- },
201
- }
202
- ```
203
-
204
- ---
205
-
206
- ## Tweener API (imperative)
207
-
208
- The `Tweener` instance is accessible via `useVoodoo`, `useTweener`, or `tweenerOptions.ref`.
209
-
210
- ### `tweener.scrollTo(axisId, position, durationMs?, easing?, noEvents?, tick?, cb?)`
211
-
212
- Animate an axis to a position over time.
213
-
214
- ### `tweener.addScrollableAnim(anim, axisId?)`
337
+ ## Core concepts in 30 seconds
215
338
 
216
- Register a tween line (array of descriptors) on a scroll axis at runtime. Returns the `CssTweenAxis` instance.
339
+ **Axis** a virtual number line. Move its position (by drag, scroll, or code) and it drives CSS animations on any number of nodes.
217
340
 
218
- ### `tweener.rmScrollableAnim(sl, axisId?)`
341
+ **Node** — a React element whose styles are controlled by one or more axes. Style updates go straight to `node.style`, no re-renders.
219
342
 
220
- Remove a previously registered tween line.
343
+ **Delta composition** — each axis contributes a *change* per frame. Stack a horizontal drag axis and a parallax axis on the same `translateX` and they simply add together. No ownership, no conflicts.
221
344
 
222
- ### `tweener.pushAnim(descriptors, durationMs, cb?, keepResults?)`
223
-
224
- Run a one-shot animation that plays to completion and then optionally releases its CSS values.
225
-
226
- ### `tweener.watchAxis(axisId, listener)`
227
-
228
- Subscribe to scroll position changes on an axis. Returns an unsubscribe function.
229
-
230
- ### `tweener.getScrollPos(axisId)`
231
-
232
- Return the current scroll position of an axis.
345
+ ```
346
+ axis position ──► tween engine ──► Δ per property ──► node.style (direct DOM write)
347
+
348
+ other axes add their Δ here
349
+ ```
233
350
 
234
351
  ---
235
352
 
236
- ## WASM acceleration in CssTweenAxis
237
-
238
- `CssTweenAxis` (the internal class used by every axis) automatically activates PROC_WASM for eligible tween descriptors:
239
-
240
- **Eligible** (WASM path):
241
- - No event callbacks (`entering` / `moving` / `leaving`).
242
- - `easeFn` is `undefined`/`null` (linear) **or** one of the 10 mapped d3-ease functions.
243
-
244
- **Not eligible** (JS path, no change in behaviour):
245
- - Descriptors with event callbacks.
246
- - Descriptors using custom easing functions not in the built-in set.
247
-
248
- The 10 d3-ease functions that map to WASM built-ins:
353
+ ## License
249
354
 
250
- | d3-ease name | WASM easing |
251
- |--------------|-------------|
252
- | *(none / linear)* | `EASE_LINEAR` |
253
- | `easeQuadIn` / `easeQuadOut` / `easeQuadInOut` | `EASE_IN/OUT/INOUT_QUAD` |
254
- | `easeCubicIn` / `easeCubicOut` / `easeCubicInOut` | `EASE_IN/OUT/INOUT_CUBIC` |
255
- | `easeExpIn` / `easeExpOut` / `easeExpInOut` | `EASE_IN/OUT/INOUT_EXPO` |
355
+ React-voodoo is dual-licensed:
256
356
 
257
- No configuration is requiredthe selection happens automatically during axis construction.
357
+ - **[CC BY-ND 4.0](http://creativecommons.org/licenses/by-nd/4.0)**use freely in commercial projects; distribution of modified versions is not permitted.
358
+ - **[AGPL v3](http://www.gnu.org/licenses/agpl-3.0)** — distribute modified versions under the same open-source license.
258
359
 
259
360
  ---
260
361
 
261
- ## Building
362
+ ## Support the project
262
363
 
263
- ```bash
264
- cd react-voodoo
265
- npm run build # production build via lpack
266
- npm run devLib # watch mode
267
- npm run setupLayers # initialise lpack layer config
268
- ```
364
+ If react-voodoo saved you a day of work, consider supporting it:
269
365
 
270
- ---
366
+ [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](#)
271
367
 
272
- ## License
368
+ **BTC** — `bc1qh43j8jh6dr8v3f675jwqq3nqymtsj8pyq0kh5a`
273
369
 
274
- Dual-licensed: **CC-BY-ND-4.0** or **AGPL-3.0-only**. See [LICENSE](./LICENSE) for details.
370
+ **PayPal** <a href="https://www.paypal.com/donate/?hosted_button_id=ECHYGKY3GR7CN"><img src="https://img.shields.io/badge/paypal-donate-yellow.svg" alt="PayPal donate" /></a>