bireactive 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/animation/anim.d.ts +57 -0
- package/dist/animation/anim.js +318 -0
- package/dist/animation/combinators.d.ts +39 -0
- package/dist/animation/combinators.js +113 -0
- package/dist/animation/easings.d.ts +5 -0
- package/dist/animation/easings.js +5 -0
- package/dist/animation/index.d.ts +3 -0
- package/dist/animation/index.js +3 -0
- package/dist/assert/algebra.d.ts +20 -0
- package/dist/assert/algebra.js +79 -0
- package/dist/assert/claim.d.ts +40 -0
- package/dist/assert/claim.js +129 -0
- package/dist/assert/index.d.ts +7 -0
- package/dist/assert/index.js +19 -0
- package/dist/assert/predicates.d.ts +18 -0
- package/dist/assert/predicates.js +43 -0
- package/dist/assert/record.d.ts +20 -0
- package/dist/assert/record.js +78 -0
- package/dist/assert/scope.d.ts +42 -0
- package/dist/assert/scope.js +233 -0
- package/dist/assert/span.d.ts +37 -0
- package/dist/assert/span.js +68 -0
- package/dist/assert/tree.d.ts +22 -0
- package/dist/assert/tree.js +65 -0
- package/dist/code/code.d.ts +70 -0
- package/dist/code/code.js +361 -0
- package/dist/code/index.d.ts +2 -0
- package/dist/code/index.js +9 -0
- package/dist/code/morph.d.ts +5 -0
- package/dist/code/morph.js +194 -0
- package/dist/code/tokenize.d.ts +8 -0
- package/dist/code/tokenize.js +51 -0
- package/dist/constraints/cluster.d.ts +83 -0
- package/dist/constraints/cluster.js +213 -0
- package/dist/constraints/drivers.d.ts +15 -0
- package/dist/constraints/drivers.js +40 -0
- package/dist/constraints/factories.d.ts +73 -0
- package/dist/constraints/factories.js +248 -0
- package/dist/constraints/index.d.ts +11 -0
- package/dist/constraints/index.js +39 -0
- package/dist/constraints/interaction.d.ts +21 -0
- package/dist/constraints/interaction.js +148 -0
- package/dist/constraints/linalg.d.ts +18 -0
- package/dist/constraints/linalg.js +141 -0
- package/dist/constraints/phases.d.ts +21 -0
- package/dist/constraints/phases.js +60 -0
- package/dist/constraints/physics.d.ts +34 -0
- package/dist/constraints/physics.js +128 -0
- package/dist/constraints/rigid.d.ts +210 -0
- package/dist/constraints/rigid.js +835 -0
- package/dist/constraints/solver.d.ts +107 -0
- package/dist/constraints/solver.js +510 -0
- package/dist/constraints/term.d.ts +50 -0
- package/dist/constraints/term.js +80 -0
- package/dist/constraints/terms.d.ts +80 -0
- package/dist/constraints/terms.js +302 -0
- package/dist/constraints/world.d.ts +31 -0
- package/dist/constraints/world.js +245 -0
- package/dist/core/aggregates.d.ts +64 -0
- package/dist/core/aggregates.js +198 -0
- package/dist/core/anim.d.ts +84 -0
- package/dist/core/anim.js +301 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +38 -0
- package/dist/core/introspect.d.ts +5 -0
- package/dist/core/introspect.js +31 -0
- package/dist/core/lenses/closed-form-policies.d.ts +64 -0
- package/dist/core/lenses/closed-form-policies.js +452 -0
- package/dist/core/lenses/domain-aggregates.d.ts +54 -0
- package/dist/core/lenses/domain-aggregates.js +259 -0
- package/dist/core/lenses/factor-lens.d.ts +42 -0
- package/dist/core/lenses/factor-lens.js +419 -0
- package/dist/core/lenses/index.d.ts +5 -0
- package/dist/core/lenses/index.js +16 -0
- package/dist/core/lenses/memory.d.ts +47 -0
- package/dist/core/lenses/memory.js +102 -0
- package/dist/core/lenses/typed-factor.d.ts +45 -0
- package/dist/core/lenses/typed-factor.js +376 -0
- package/dist/core/network-utils.d.ts +14 -0
- package/dist/core/network-utils.js +62 -0
- package/dist/core/new-primitives.d.ts +33 -0
- package/dist/core/new-primitives.js +113 -0
- package/dist/core/signal.d.ts +254 -0
- package/dist/core/signal.js +1349 -0
- package/dist/core/traits.d.ts +61 -0
- package/dist/core/traits.js +56 -0
- package/dist/core/tree.d.ts +23 -0
- package/dist/core/tree.js +62 -0
- package/dist/core/values/anchor.d.ts +23 -0
- package/dist/core/values/anchor.js +23 -0
- package/dist/core/values/audio.d.ts +33 -0
- package/dist/core/values/audio.js +107 -0
- package/dist/core/values/bool.d.ts +37 -0
- package/dist/core/values/bool.js +75 -0
- package/dist/core/values/box.d.ts +77 -0
- package/dist/core/values/box.js +211 -0
- package/dist/core/values/canvas.d.ts +71 -0
- package/dist/core/values/canvas.js +495 -0
- package/dist/core/values/color.d.ts +49 -0
- package/dist/core/values/color.js +106 -0
- package/dist/core/values/flags.d.ts +18 -0
- package/dist/core/values/flags.js +50 -0
- package/dist/core/values/gpu.d.ts +74 -0
- package/dist/core/values/gpu.js +426 -0
- package/dist/core/values/matrix.d.ts +53 -0
- package/dist/core/values/matrix.js +140 -0
- package/dist/core/values/num.d.ts +62 -0
- package/dist/core/values/num.js +166 -0
- package/dist/core/values/pose.d.ts +31 -0
- package/dist/core/values/pose.js +83 -0
- package/dist/core/values/range.d.ts +83 -0
- package/dist/core/values/range.js +167 -0
- package/dist/core/values/str.d.ts +76 -0
- package/dist/core/values/str.js +346 -0
- package/dist/core/values/template.d.ts +49 -0
- package/dist/core/values/template.js +148 -0
- package/dist/core/values/transform.d.ts +49 -0
- package/dist/core/values/transform.js +115 -0
- package/dist/core/values/tri.d.ts +31 -0
- package/dist/core/values/tri.js +95 -0
- package/dist/core/values/vec.d.ts +72 -0
- package/dist/core/values/vec.js +219 -0
- package/dist/core/writable.d.ts +15 -0
- package/dist/core/writable.js +29 -0
- package/dist/ext/events.d.ts +10 -0
- package/dist/ext/events.js +31 -0
- package/dist/ext/index.d.ts +4 -0
- package/dist/ext/index.js +4 -0
- package/dist/ext/snapshot.d.ts +8 -0
- package/dist/ext/snapshot.js +29 -0
- package/dist/ext/timeline.d.ts +56 -0
- package/dist/ext/timeline.js +94 -0
- package/dist/ext/waapi.d.ts +25 -0
- package/dist/ext/waapi.js +198 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/propagators/index.d.ts +6 -0
- package/dist/propagators/index.js +6 -0
- package/dist/propagators/layout.d.ts +68 -0
- package/dist/propagators/layout.js +336 -0
- package/dist/propagators/network.d.ts +52 -0
- package/dist/propagators/network.js +185 -0
- package/dist/propagators/propagator.d.ts +12 -0
- package/dist/propagators/propagator.js +16 -0
- package/dist/propagators/range.d.ts +45 -0
- package/dist/propagators/range.js +147 -0
- package/dist/propagators/relations.d.ts +60 -0
- package/dist/propagators/relations.js +343 -0
- package/dist/shapes/annular-sector.d.ts +15 -0
- package/dist/shapes/annular-sector.js +64 -0
- package/dist/shapes/button.d.ts +14 -0
- package/dist/shapes/button.js +31 -0
- package/dist/shapes/choreographers.d.ts +22 -0
- package/dist/shapes/choreographers.js +69 -0
- package/dist/shapes/circle.d.ts +17 -0
- package/dist/shapes/circle.js +57 -0
- package/dist/shapes/clip.d.ts +5 -0
- package/dist/shapes/clip.js +31 -0
- package/dist/shapes/connect.d.ts +16 -0
- package/dist/shapes/connect.js +70 -0
- package/dist/shapes/curve.d.ts +60 -0
- package/dist/shapes/curve.js +285 -0
- package/dist/shapes/dashed.d.ts +16 -0
- package/dist/shapes/dashed.js +142 -0
- package/dist/shapes/debug.d.ts +43 -0
- package/dist/shapes/debug.js +97 -0
- package/dist/shapes/group.d.ts +5 -0
- package/dist/shapes/group.js +10 -0
- package/dist/shapes/handle.d.ts +32 -0
- package/dist/shapes/handle.js +88 -0
- package/dist/shapes/index.d.ts +23 -0
- package/dist/shapes/index.js +23 -0
- package/dist/shapes/interaction.d.ts +32 -0
- package/dist/shapes/interaction.js +187 -0
- package/dist/shapes/label.d.ts +20 -0
- package/dist/shapes/label.js +42 -0
- package/dist/shapes/layout.d.ts +29 -0
- package/dist/shapes/layout.js +74 -0
- package/dist/shapes/line.d.ts +21 -0
- package/dist/shapes/line.js +79 -0
- package/dist/shapes/list.d.ts +18 -0
- package/dist/shapes/list.js +51 -0
- package/dist/shapes/mount.d.ts +7 -0
- package/dist/shapes/mount.js +10 -0
- package/dist/shapes/path.d.ts +77 -0
- package/dist/shapes/path.js +227 -0
- package/dist/shapes/rect.d.ts +30 -0
- package/dist/shapes/rect.js +131 -0
- package/dist/shapes/shape.d.ts +132 -0
- package/dist/shapes/shape.js +306 -0
- package/dist/shapes/text.d.ts +24 -0
- package/dist/shapes/text.js +53 -0
- package/dist/shapes/tokens.d.ts +28 -0
- package/dist/shapes/tokens.js +27 -0
- package/dist/shapes/transitions.d.ts +23 -0
- package/dist/shapes/transitions.js +62 -0
- package/dist/tex/decorations.d.ts +26 -0
- package/dist/tex/decorations.js +116 -0
- package/dist/tex/index.d.ts +5 -0
- package/dist/tex/index.js +5 -0
- package/dist/tex/marker.d.ts +17 -0
- package/dist/tex/marker.js +63 -0
- package/dist/tex/motion.d.ts +43 -0
- package/dist/tex/motion.js +290 -0
- package/dist/tex/parts.d.ts +65 -0
- package/dist/tex/parts.js +149 -0
- package/dist/tex/tex.d.ts +45 -0
- package/dist/tex/tex.js +244 -0
- package/dist/web/attr.d.ts +16 -0
- package/dist/web/attr.js +98 -0
- package/dist/web/diagram.d.ts +49 -0
- package/dist/web/diagram.js +260 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +6 -0
- package/dist/web/md-marker.d.ts +6 -0
- package/dist/web/md-marker.js +39 -0
- package/dist/web/md-tex.d.ts +6 -0
- package/dist/web/md-tex.js +61 -0
- package/dist/web/raf.d.ts +6 -0
- package/dist/web/raf.js +24 -0
- package/dist/web/viewport.d.ts +7 -0
- package/dist/web/viewport.js +13 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orion Reed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# bireactive
|
|
2
|
+
|
|
3
|
+
Reactive programming where edges can go both ways. Write to an input and
|
|
4
|
+
everything derived from it updates; write the *derived* value and the input
|
|
5
|
+
adjusts to match. Forward and backward propagation are handled by the engine, with the same set of caveats as regular reactive programming.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install bireactive
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Runtime dependencies [`temml`](https://temml.org) (for `tex`) and
|
|
14
|
+
[`prism-esm`](https://github.com/orionhealthotago/prism-esm) (for `code`) are
|
|
15
|
+
installed automatically. They may be split into separate packages later so the
|
|
16
|
+
core stays dependency-free.
|
|
17
|
+
|
|
18
|
+
## Sketch
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { cell } from "bireactive";
|
|
22
|
+
|
|
23
|
+
// A derived value with an inverse — the edge runs both ways.
|
|
24
|
+
const celsius = cell(20);
|
|
25
|
+
const fahrenheit = celsius.lens(
|
|
26
|
+
c => (c * 9) / 5 + 32, // forward
|
|
27
|
+
f => ((f - 32) * 5) / 9, // backward
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
fahrenheit.value; // 68
|
|
31
|
+
fahrenheit.value = 212; // write the derived end…
|
|
32
|
+
celsius.value; // 100 — …and the source updates to match
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Values also come as small classes (`Num`, `Vec`, `Box`, `Color`, ...) with field
|
|
36
|
+
lenses and bidirectional operators.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { vec, box, midpointLens } from "bireactive";
|
|
40
|
+
|
|
41
|
+
// Free-function lens: the midpoint of two points, writable.
|
|
42
|
+
const a = vec(0, 0);
|
|
43
|
+
const b = vec(10, 0);
|
|
44
|
+
const mid = midpointLens(a, b);
|
|
45
|
+
mid.value = { x: 5, y: 10 }; // drag it up…
|
|
46
|
+
a.value; // { x: 0, y: 10 } — both ends translate to keep it the midpoint
|
|
47
|
+
|
|
48
|
+
// Chaining value-class operators builds a multi-step writable view.
|
|
49
|
+
const p = vec(10, 20);
|
|
50
|
+
const view = p.scale(2).right(5); // ×2, then shift +5 in x
|
|
51
|
+
view.value; // { x: 25, y: 40 }
|
|
52
|
+
view.value = { x: 5, y: 0 }; // write the end of the chain…
|
|
53
|
+
p.value; // { x: 0, y: 0 } — inverted back through right, then scale
|
|
54
|
+
|
|
55
|
+
// Cross-type lens: a Box/Vec relation projected to Bool, still writable.
|
|
56
|
+
const region = box(0, 0, 100, 100); // x, y, w, h
|
|
57
|
+
const q = vec(150, 50); // outside the box
|
|
58
|
+
const inside = region.contains(q); // Bool view of "q ∈ region"
|
|
59
|
+
inside.value; // false
|
|
60
|
+
inside.value = true; // assert membership…
|
|
61
|
+
q.value; // { x: 100, y: 50 } — q snaps to the nearest in-box point
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Develop
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npm run dev # serve the landing page at :5555
|
|
68
|
+
npm run site # build the static site into dist-web/
|
|
69
|
+
npm run build # compile the library into dist/
|
|
70
|
+
npm test # run the test suite
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Status
|
|
74
|
+
|
|
75
|
+
`0.x` — APIs are still moving. The package is a single bundle today;
|
|
76
|
+
sub-packages (`@bireactive/core`, `@bireactive/animation`, `@bireactive/shapes`, …) are used
|
|
77
|
+
internally as path aliases and will be split out once the surface settles.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface Tick {
|
|
2
|
+
readonly dt: number;
|
|
3
|
+
readonly elapsed: number;
|
|
4
|
+
}
|
|
5
|
+
export type Yieldable = undefined | number | Animator<any> | readonly Yieldable[] | Suspend<any>;
|
|
6
|
+
export type Animator<R = void> = Generator<Yieldable, R, Tick>;
|
|
7
|
+
export type Suspend<T = void> = (wake: Wake<T>,
|
|
8
|
+
/** Spawn `g` at engine root, returning a cancel handle. **/
|
|
9
|
+
spawn: (g: Animator<any>) => () => void) => void | (() => void);
|
|
10
|
+
export type Resume<Y> = Y extends Animator<infer R> ? R : Y extends Suspend<infer R> ? R : void;
|
|
11
|
+
export type Cut<T> = {
|
|
12
|
+
readonly [CUT_KEY]: T;
|
|
13
|
+
};
|
|
14
|
+
/** Cut sentinel — return `cut(v)` from a concurrent kid to settle the
|
|
15
|
+
* enclosing group with `v` and cancel siblings. Outside a group, the
|
|
16
|
+
* sentinel is transparently unwrapped to `v`. */
|
|
17
|
+
export declare const cut: <T>(value: T) => Cut<T>;
|
|
18
|
+
/** True if `v` is a Generator (duck-typed via `.next`). */
|
|
19
|
+
export declare const isGenerator: (v: unknown) => v is Animator;
|
|
20
|
+
export declare class Anim {
|
|
21
|
+
#private;
|
|
22
|
+
private actives;
|
|
23
|
+
private deads;
|
|
24
|
+
/** Re-entry guard: nested `step()` throws. start/stop/cancel stay legal
|
|
25
|
+
* (they only mutate `actives`, handled via index + skip-checks). */
|
|
26
|
+
private stepping;
|
|
27
|
+
private stepListeners;
|
|
28
|
+
private onError;
|
|
29
|
+
get clock(): number;
|
|
30
|
+
constructor(opts?: {
|
|
31
|
+
onError?: (e: unknown) => void;
|
|
32
|
+
});
|
|
33
|
+
/** Spawn root-level actives; the returned handle cancels all. (Inside
|
|
34
|
+
* a gen, `yield [a, b]` instead for a joined, cascading-cancel group.) */
|
|
35
|
+
start(...gs: Animator<any>[]): () => void;
|
|
36
|
+
/** Fire `cb(dt)` after every successful `step()` completes. */
|
|
37
|
+
onStep(cb: (dt: number) => void): () => void;
|
|
38
|
+
stop(): void;
|
|
39
|
+
step(dt: number): void;
|
|
40
|
+
private stepInner;
|
|
41
|
+
private spawn;
|
|
42
|
+
private cancel;
|
|
43
|
+
private settle;
|
|
44
|
+
private safe;
|
|
45
|
+
private compact;
|
|
46
|
+
private advance;
|
|
47
|
+
private suspend;
|
|
48
|
+
/** Park `a` and spawn `gen` as its child; resume `a` with the
|
|
49
|
+
* child's return value (or error) on settle. */
|
|
50
|
+
private awaitChild;
|
|
51
|
+
private concurrent;
|
|
52
|
+
}
|
|
53
|
+
declare const CUT_KEY: unique symbol;
|
|
54
|
+
type Wake<T = void> = ([T] extends [void] ? () => void : (value: T) => void) & {
|
|
55
|
+
throw(error: unknown): void;
|
|
56
|
+
};
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Yield contract:
|
|
2
|
+
// undefined park 1 frame
|
|
3
|
+
// number > 0 sleep N seconds
|
|
4
|
+
// Animator spawn child, await its return value
|
|
5
|
+
// Suspend callback-wake `(wake, spawn) => dispose`
|
|
6
|
+
// Yieldable[] run concurrently; resume with results[]
|
|
7
|
+
//
|
|
8
|
+
// Resume values are either:
|
|
9
|
+
// Tick, a { dt: number, elapsed: number } object
|
|
10
|
+
// Example: `const { dt } = yield;`
|
|
11
|
+
//
|
|
12
|
+
// T (generic), if the suspend is `(wake, spawn) => T`
|
|
13
|
+
// Example: `const result = yield (wake, spawn) => { ... }`
|
|
14
|
+
/** Cut sentinel — return `cut(v)` from a concurrent kid to settle the
|
|
15
|
+
* enclosing group with `v` and cancel siblings. Outside a group, the
|
|
16
|
+
* sentinel is transparently unwrapped to `v`. */
|
|
17
|
+
export const cut = (value) => ({ [CUT_KEY]: value });
|
|
18
|
+
/** True if `v` is a Generator (duck-typed via `.next`). */
|
|
19
|
+
export const isGenerator = (v) => v !== null && typeof v === "object" && typeof v.next === "function";
|
|
20
|
+
export class Anim {
|
|
21
|
+
actives = [];
|
|
22
|
+
deads = 0;
|
|
23
|
+
/** Re-entry guard: nested `step()` throws. start/stop/cancel stay legal
|
|
24
|
+
* (they only mutate `actives`, handled via index + skip-checks). */
|
|
25
|
+
stepping = false;
|
|
26
|
+
stepListeners = null;
|
|
27
|
+
onError;
|
|
28
|
+
#clock = 0;
|
|
29
|
+
get clock() {
|
|
30
|
+
return this.#clock;
|
|
31
|
+
}
|
|
32
|
+
constructor(opts = {}) {
|
|
33
|
+
this.onError = opts.onError ?? (e => console.error("bireactive:", e));
|
|
34
|
+
}
|
|
35
|
+
/** Spawn root-level actives; the returned handle cancels all. (Inside
|
|
36
|
+
* a gen, `yield [a, b]` instead for a joined, cascading-cancel group.) */
|
|
37
|
+
start(...gs) {
|
|
38
|
+
if (gs.length === 0)
|
|
39
|
+
return () => { };
|
|
40
|
+
const actives = gs.map(g => this.spawn(g, null, null));
|
|
41
|
+
return () => {
|
|
42
|
+
for (const a of actives)
|
|
43
|
+
this.cancel(a);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Fire `cb(dt)` after every successful `step()` completes. */
|
|
47
|
+
onStep(cb) {
|
|
48
|
+
(this.stepListeners ??= new Set()).add(cb);
|
|
49
|
+
return () => {
|
|
50
|
+
this.stepListeners?.delete(cb);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
stop() {
|
|
54
|
+
const snap = this.actives.slice();
|
|
55
|
+
this.actives.length = 0;
|
|
56
|
+
for (const a of snap)
|
|
57
|
+
this.cancel(a);
|
|
58
|
+
}
|
|
59
|
+
step(dt) {
|
|
60
|
+
if (this.stepping) {
|
|
61
|
+
throw new Error("bireactive: re-entrant step() is not supported");
|
|
62
|
+
}
|
|
63
|
+
this.stepping = true;
|
|
64
|
+
try {
|
|
65
|
+
this.stepInner(dt);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
this.stepping = false;
|
|
69
|
+
}
|
|
70
|
+
if (this.stepListeners) {
|
|
71
|
+
for (const cb of this.stepListeners) {
|
|
72
|
+
try {
|
|
73
|
+
cb(dt);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
this.onError(e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
stepInner(dt) {
|
|
82
|
+
if (dt > 0 && Number.isFinite(dt))
|
|
83
|
+
this.#clock += dt;
|
|
84
|
+
const as = this.actives;
|
|
85
|
+
const alen = as.length;
|
|
86
|
+
const d0 = this.deads;
|
|
87
|
+
for (let i = 0; i < alen; i++) {
|
|
88
|
+
const a = as[i];
|
|
89
|
+
if (!a || a.wakeAt === DEAD || a.wakeAt === PARKED)
|
|
90
|
+
continue;
|
|
91
|
+
if (dt > 0)
|
|
92
|
+
a.localClock += dt;
|
|
93
|
+
if (a.wakeAt <= a.localClock) {
|
|
94
|
+
const saved = a.wakeAt;
|
|
95
|
+
a.wakeAt = READY;
|
|
96
|
+
// Sub-frame: only the time since the wake threshold is "owed".
|
|
97
|
+
const dtEff = saved > 0 ? Math.min(dt, a.localClock - saved) : dt;
|
|
98
|
+
const tick = { dt: dtEff, elapsed: a.localClock };
|
|
99
|
+
this.advance(a, tick, false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (this.deads !== d0)
|
|
103
|
+
this.compact();
|
|
104
|
+
}
|
|
105
|
+
spawn(gen, parent, onSettle) {
|
|
106
|
+
const a = new Active(gen);
|
|
107
|
+
a.onSettle = onSettle;
|
|
108
|
+
a.localClock = parent ? parent.localClock : 0;
|
|
109
|
+
this.actives.push(a);
|
|
110
|
+
this.advance(a, undefined, false);
|
|
111
|
+
return a;
|
|
112
|
+
}
|
|
113
|
+
cancel(a) {
|
|
114
|
+
if (a.wakeAt === DEAD)
|
|
115
|
+
return;
|
|
116
|
+
a.wakeAt = DEAD;
|
|
117
|
+
this.deads++;
|
|
118
|
+
const c = a.cleanup;
|
|
119
|
+
a.cleanup = null;
|
|
120
|
+
a.onSettle = null;
|
|
121
|
+
this.safe(c);
|
|
122
|
+
try {
|
|
123
|
+
a.gen.return(undefined);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
this.onError(e);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
settle(a, value, errored, error) {
|
|
130
|
+
if (a.wakeAt === DEAD)
|
|
131
|
+
return;
|
|
132
|
+
a.wakeAt = DEAD;
|
|
133
|
+
this.deads++;
|
|
134
|
+
const cb = a.onSettle;
|
|
135
|
+
a.onSettle = null;
|
|
136
|
+
if (cb)
|
|
137
|
+
cb(errored ? undefined : value, errored ? error : undefined);
|
|
138
|
+
else if (errored)
|
|
139
|
+
this.onError(error);
|
|
140
|
+
}
|
|
141
|
+
safe(fn) {
|
|
142
|
+
if (!fn)
|
|
143
|
+
return;
|
|
144
|
+
try {
|
|
145
|
+
fn();
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
this.onError(e);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
compact() {
|
|
152
|
+
const as = this.actives;
|
|
153
|
+
let w = 0;
|
|
154
|
+
for (let i = 0; i < as.length; i++)
|
|
155
|
+
if (as[i].wakeAt !== DEAD)
|
|
156
|
+
as[w++] = as[i];
|
|
157
|
+
as.length = w;
|
|
158
|
+
this.deads = 0;
|
|
159
|
+
}
|
|
160
|
+
advance(a, payload, asThrow) {
|
|
161
|
+
try {
|
|
162
|
+
const r = asThrow ? a.gen.throw(payload) : a.gen.next(payload);
|
|
163
|
+
while (!r.done) {
|
|
164
|
+
if (a.wakeAt === DEAD)
|
|
165
|
+
return;
|
|
166
|
+
const v = r.value;
|
|
167
|
+
if (v === undefined)
|
|
168
|
+
return; // park 1 frame
|
|
169
|
+
if (typeof v === "number") {
|
|
170
|
+
// `yield N <= 0` parks (semantic alignment with `yield`).
|
|
171
|
+
if (v > 0)
|
|
172
|
+
a.wakeAt = a.localClock + v;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (typeof v === "function")
|
|
176
|
+
return this.suspend(a, v);
|
|
177
|
+
if (Array.isArray(v))
|
|
178
|
+
return this.concurrent(a, v);
|
|
179
|
+
if (isGenerator(v))
|
|
180
|
+
return this.awaitChild(a, v);
|
|
181
|
+
throw new TypeError(`anim: unsupported yield (${describe(v)})`);
|
|
182
|
+
}
|
|
183
|
+
this.settle(a, r.value, false, undefined);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
this.settle(a, undefined, true, e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
suspend(a, impl) {
|
|
190
|
+
let resumed = false;
|
|
191
|
+
const finish = (action) => {
|
|
192
|
+
if (resumed || a.wakeAt === DEAD)
|
|
193
|
+
return;
|
|
194
|
+
resumed = true;
|
|
195
|
+
const c = a.cleanup;
|
|
196
|
+
a.cleanup = null;
|
|
197
|
+
a.wakeAt = READY;
|
|
198
|
+
this.safe(c);
|
|
199
|
+
action();
|
|
200
|
+
};
|
|
201
|
+
const wake = ((v) => finish(() => this.advance(a, unwrapCut(v), false)));
|
|
202
|
+
wake.throw = (e) => finish(() => this.advance(a, e, true));
|
|
203
|
+
const spawn = (g) => {
|
|
204
|
+
const child = this.spawn(g, null, null);
|
|
205
|
+
return () => this.cancel(child);
|
|
206
|
+
};
|
|
207
|
+
let dispose;
|
|
208
|
+
try {
|
|
209
|
+
dispose = impl(wake, spawn) ?? undefined;
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
if (!resumed && a.wakeAt !== DEAD) {
|
|
213
|
+
resumed = true;
|
|
214
|
+
this.advance(a, e, true);
|
|
215
|
+
}
|
|
216
|
+
else
|
|
217
|
+
this.onError(e);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (resumed || a.wakeAt === DEAD)
|
|
221
|
+
this.safe(dispose);
|
|
222
|
+
else {
|
|
223
|
+
a.wakeAt = PARKED;
|
|
224
|
+
a.cleanup = dispose ?? null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Park `a` and spawn `gen` as its child; resume `a` with the
|
|
228
|
+
* child's return value (or error) on settle. */
|
|
229
|
+
awaitChild(a, gen) {
|
|
230
|
+
a.wakeAt = PARKED;
|
|
231
|
+
let c = null;
|
|
232
|
+
a.cleanup = () => {
|
|
233
|
+
if (c && c.wakeAt !== DEAD)
|
|
234
|
+
this.cancel(c);
|
|
235
|
+
};
|
|
236
|
+
c = this.spawn(gen, a, (v, err) => {
|
|
237
|
+
if (a.wakeAt === DEAD || a.cleanup === null)
|
|
238
|
+
return;
|
|
239
|
+
a.cleanup = null;
|
|
240
|
+
a.wakeAt = READY;
|
|
241
|
+
this.advance(a, err === undefined ? unwrapCut(v) : err, err !== undefined);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
concurrent(a, kids) {
|
|
245
|
+
if (kids.length === 0)
|
|
246
|
+
return this.advance(a, [], false);
|
|
247
|
+
const children = [];
|
|
248
|
+
const results = new Array(kids.length);
|
|
249
|
+
let left = kids.length;
|
|
250
|
+
let aborted = false;
|
|
251
|
+
a.wakeAt = PARKED;
|
|
252
|
+
a.cleanup = () => {
|
|
253
|
+
aborted = true;
|
|
254
|
+
for (const c of children)
|
|
255
|
+
if (c.wakeAt !== DEAD)
|
|
256
|
+
this.cancel(c);
|
|
257
|
+
};
|
|
258
|
+
const settle = (v, asThrow, cancelSibs) => {
|
|
259
|
+
if (aborted)
|
|
260
|
+
return;
|
|
261
|
+
aborted = true;
|
|
262
|
+
a.cleanup = null;
|
|
263
|
+
a.wakeAt = READY;
|
|
264
|
+
if (cancelSibs)
|
|
265
|
+
for (const c of children)
|
|
266
|
+
if (c.wakeAt !== DEAD)
|
|
267
|
+
this.cancel(c);
|
|
268
|
+
this.advance(a, v, asThrow);
|
|
269
|
+
};
|
|
270
|
+
for (let j = 0; j < kids.length; j++) {
|
|
271
|
+
if (aborted)
|
|
272
|
+
return;
|
|
273
|
+
const k = kids[j];
|
|
274
|
+
const idx = j;
|
|
275
|
+
const kidGen = isGenerator(k) ? k : asGen(k);
|
|
276
|
+
children.push(this.spawn(kidGen, a, (value, error) => {
|
|
277
|
+
if (aborted)
|
|
278
|
+
return;
|
|
279
|
+
if (error !== undefined)
|
|
280
|
+
return settle(error, true, true);
|
|
281
|
+
if (isCut(value))
|
|
282
|
+
return settle(value[CUT_KEY], false, true);
|
|
283
|
+
results[idx] = value;
|
|
284
|
+
if (--left === 0)
|
|
285
|
+
settle(results, false, false);
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const DEAD = Number.NEGATIVE_INFINITY;
|
|
291
|
+
const READY = 0;
|
|
292
|
+
const PARKED = Number.POSITIVE_INFINITY;
|
|
293
|
+
const CUT_KEY = Symbol("cut");
|
|
294
|
+
const isCut = (v) => v !== null && typeof v === "object" && CUT_KEY in v;
|
|
295
|
+
const unwrapCut = (v) => (isCut(v) ? v[CUT_KEY] : v);
|
|
296
|
+
class Active {
|
|
297
|
+
gen;
|
|
298
|
+
/** READY (0) | PARKED (Inf) | DEAD (-Inf) | positive sleep target. */
|
|
299
|
+
wakeAt = READY;
|
|
300
|
+
/** Per-active subjective clock — advances by engine dt each step.
|
|
301
|
+
* Inherited from parent on spawn, then advances independently. */
|
|
302
|
+
localClock = 0;
|
|
303
|
+
cleanup = null;
|
|
304
|
+
onSettle = null;
|
|
305
|
+
constructor(gen) {
|
|
306
|
+
this.gen = gen;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function* asGen(y) {
|
|
310
|
+
yield y;
|
|
311
|
+
}
|
|
312
|
+
function describe(v) {
|
|
313
|
+
if (v === null)
|
|
314
|
+
return "null";
|
|
315
|
+
if (typeof v !== "object")
|
|
316
|
+
return String(v);
|
|
317
|
+
return v.constructor?.name ?? "object";
|
|
318
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Animator, type Cut, type Resume, type Suspend, type Tick, type Yieldable } from "./anim.js";
|
|
2
|
+
/** Park each frame until `cb` returns `false`. `t` is elapsed since the
|
|
3
|
+
* first call (sampled from `tick.elapsed` — no float accumulation). */
|
|
4
|
+
export declare function drive(cb: (tick: Tick, t: number) => boolean | void): Animator<void>;
|
|
5
|
+
/** Park until `wake(value)`; resume with the typed value. */
|
|
6
|
+
export declare function suspend<T = void>(impl: Suspend<T>): Animator<T>;
|
|
7
|
+
/** Wait for a DOM event on `target`; resume with the event. */
|
|
8
|
+
export declare function untilEvent<E extends Event = Event>(target: EventTarget, name: string, opts?: AddEventListenerOptions): Animator<E>;
|
|
9
|
+
/** Wait for a promise; resume with its value (rejection → `gen.throw`). */
|
|
10
|
+
export declare function untilPromise<T>(p: PromiseLike<T>): Animator<T>;
|
|
11
|
+
/** Wrap a Yieldable so it cuts its enclosing group with its result. */
|
|
12
|
+
export declare function commit<T>(k: Yieldable): Animator<Cut<T>>;
|
|
13
|
+
/** Run children in parallel; resume with a typed tuple of return values. */
|
|
14
|
+
export declare function all<Cs extends readonly Yieldable[]>(...children: Cs): Animator<{
|
|
15
|
+
[K in keyof Cs]: Resume<Cs[K]>;
|
|
16
|
+
}>;
|
|
17
|
+
/** First-completion race; resume with the winner's payload. */
|
|
18
|
+
export declare function race<Cs extends readonly Yieldable[]>(...children: Cs): Animator<Resume<Cs[number]>>;
|
|
19
|
+
/** First N completions win; resume with their values in completion order. */
|
|
20
|
+
export declare function firstN<R>(n: number, kids: readonly Yieldable[]): Animator<R[]>;
|
|
21
|
+
/** First kid whose value matches `pred` cuts the group with it;
|
|
22
|
+
* otherwise settles with the full results array. */
|
|
23
|
+
export declare function firstMatching<R>(pred: (v: R) => boolean, kids: readonly Yieldable[]): Animator<R | R[]>;
|
|
24
|
+
/** First kid to resolve wins; all-throw → `AggregateError` (~ `Promise.any`). */
|
|
25
|
+
export declare function anySuccess<R>(...kids: readonly Yieldable[]): Animator<R>;
|
|
26
|
+
/** Run every kid; collect results and errors. Never throws. */
|
|
27
|
+
export type Settled<R> = {
|
|
28
|
+
readonly ok: true;
|
|
29
|
+
readonly value: R;
|
|
30
|
+
} | {
|
|
31
|
+
readonly ok: false;
|
|
32
|
+
readonly error: unknown;
|
|
33
|
+
};
|
|
34
|
+
export declare function allSettled<R>(...kids: readonly Yieldable[]): Animator<Settled<R>[]>;
|
|
35
|
+
/** Pick one child uniformly at random and run it; others never advance. */
|
|
36
|
+
export declare function rand(...children: Animator[]): Animator;
|
|
37
|
+
/** Spawn `g` at engine root, resume parent immediately. Detached child
|
|
38
|
+
* outlives the spawning parent (survives parent cancel; dies on engine.stop()). */
|
|
39
|
+
export declare function detach<R>(g: Animator<R>): Animator<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Cell-free combinators over `Anim`. Concurrency rules are expressed
|
|
2
|
+
// per-kid via `cut(v)` — no engine-side strategy abstraction.
|
|
3
|
+
import { cut, } from "./anim.js";
|
|
4
|
+
/** Park each frame until `cb` returns `false`. `t` is elapsed since the
|
|
5
|
+
* first call (sampled from `tick.elapsed` — no float accumulation). */
|
|
6
|
+
export function* drive(cb) {
|
|
7
|
+
let startElapsed = Number.NaN;
|
|
8
|
+
while (true) {
|
|
9
|
+
const tick = yield;
|
|
10
|
+
if (startElapsed !== startElapsed)
|
|
11
|
+
startElapsed = tick.elapsed - tick.dt;
|
|
12
|
+
if (cb(tick, tick.elapsed - startElapsed) === false)
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Park until `wake(value)`; resume with the typed value. */
|
|
17
|
+
export function* suspend(impl) {
|
|
18
|
+
return (yield impl);
|
|
19
|
+
}
|
|
20
|
+
/** Wait for a DOM event on `target`; resume with the event. */
|
|
21
|
+
export function untilEvent(target, name, opts) {
|
|
22
|
+
return suspend(wake => {
|
|
23
|
+
const handler = (e) => wake(e);
|
|
24
|
+
target.addEventListener(name, handler, opts);
|
|
25
|
+
return () => target.removeEventListener(name, handler, opts);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Wait for a promise; resume with its value (rejection → `gen.throw`). */
|
|
29
|
+
export function untilPromise(p) {
|
|
30
|
+
return suspend(wake => {
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
p.then(v => {
|
|
33
|
+
if (!cancelled)
|
|
34
|
+
wake(v);
|
|
35
|
+
}, e => {
|
|
36
|
+
if (!cancelled)
|
|
37
|
+
wake.throw(e);
|
|
38
|
+
});
|
|
39
|
+
return () => {
|
|
40
|
+
cancelled = true;
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Wrap a Yieldable so it cuts its enclosing group with its result. */
|
|
45
|
+
export function* commit(k) {
|
|
46
|
+
return cut((yield k));
|
|
47
|
+
}
|
|
48
|
+
/** Run children in parallel; resume with a typed tuple of return values. */
|
|
49
|
+
export function* all(...children) {
|
|
50
|
+
return (yield children);
|
|
51
|
+
}
|
|
52
|
+
/** First-completion race; resume with the winner's payload. */
|
|
53
|
+
export function* race(...children) {
|
|
54
|
+
return (yield children.map(c => commit(c)));
|
|
55
|
+
}
|
|
56
|
+
/** First N completions win; resume with their values in completion order. */
|
|
57
|
+
export function* firstN(n, kids) {
|
|
58
|
+
const collected = [];
|
|
59
|
+
return (yield kids.map(k => (function* () {
|
|
60
|
+
const v = (yield k);
|
|
61
|
+
collected.push(v);
|
|
62
|
+
return collected.length >= n ? cut(collected) : v;
|
|
63
|
+
})()));
|
|
64
|
+
}
|
|
65
|
+
/** First kid whose value matches `pred` cuts the group with it;
|
|
66
|
+
* otherwise settles with the full results array. */
|
|
67
|
+
export function* firstMatching(pred, kids) {
|
|
68
|
+
return (yield kids.map(k => (function* () {
|
|
69
|
+
const v = (yield k);
|
|
70
|
+
return pred(v) ? cut(v) : v;
|
|
71
|
+
})()));
|
|
72
|
+
}
|
|
73
|
+
/** First kid to resolve wins; all-throw → `AggregateError` (~ `Promise.any`). */
|
|
74
|
+
export function* anySuccess(...kids) {
|
|
75
|
+
const errors = [];
|
|
76
|
+
return (yield kids.map(k => (function* () {
|
|
77
|
+
try {
|
|
78
|
+
return cut((yield k));
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
errors.push(e);
|
|
82
|
+
if (errors.length === kids.length) {
|
|
83
|
+
throw new AggregateError(errors, "anySuccess: all kids failed");
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
})()));
|
|
88
|
+
}
|
|
89
|
+
export function* allSettled(...kids) {
|
|
90
|
+
return (yield kids.map(k => (function* () {
|
|
91
|
+
try {
|
|
92
|
+
return { ok: true, value: (yield k) };
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
return { ok: false, error: e };
|
|
96
|
+
}
|
|
97
|
+
})()));
|
|
98
|
+
}
|
|
99
|
+
/** Pick one child uniformly at random and run it; others never advance. */
|
|
100
|
+
export function* rand(...children) {
|
|
101
|
+
if (children.length === 0)
|
|
102
|
+
return;
|
|
103
|
+
const i = Math.floor(Math.random() * children.length);
|
|
104
|
+
yield* children[i];
|
|
105
|
+
}
|
|
106
|
+
/** Spawn `g` at engine root, resume parent immediately. Detached child
|
|
107
|
+
* outlives the spawning parent (survives parent cancel; dies on engine.stop()). */
|
|
108
|
+
export function* detach(g) {
|
|
109
|
+
yield (wake, spawn) => {
|
|
110
|
+
spawn(g);
|
|
111
|
+
wake();
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { Anim, type Animator, type Cut, cut, isGenerator, type Resume, type Suspend, type Tick, type Yieldable, } from "./anim.js";
|
|
2
|
+
export { all, allSettled, anySuccess, commit, detach, drive, firstMatching, firstN, race, rand, type Settled, suspend, untilEvent, untilPromise, } from "./combinators.js";
|
|
3
|
+
export { type Easing, easeIn, easeInOut, easeOut, linear } from "./easings.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { Anim, cut, isGenerator, } from "./anim.js";
|
|
2
|
+
export { all, allSettled, anySuccess, commit, detach, drive, firstMatching, firstN, race, rand, suspend, untilEvent, untilPromise, } from "./combinators.js";
|
|
3
|
+
export { easeIn, easeInOut, easeOut, linear } from "./easings.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Read } from "../core/index.js";
|
|
2
|
+
import type { Scoped } from "./scope.js";
|
|
3
|
+
import type { Span } from "./span.js";
|
|
4
|
+
/** Anything an interval can be derived from. */
|
|
5
|
+
export type Scope = Scoped<any> | Span | Read<boolean>;
|
|
6
|
+
/** "Is this scope open right now?" Spans → one-shot interval; scoped
|
|
7
|
+
* factories → "any open invocation"; bool signals pass through. */
|
|
8
|
+
export declare function intervals(s: Scope): Read<boolean>;
|
|
9
|
+
/** Always-true scope; useful as a default when no scope is given. */
|
|
10
|
+
export declare function always(): Read<boolean>;
|
|
11
|
+
/** Latch a predicate. Holds at `init` until `pred` is observed `!init`
|
|
12
|
+
* within `scope`, then flips and stays; re-arms on each `scope` rising
|
|
13
|
+
* edge. Outside `scope`, `pred` isn't consulted (latch holds). */
|
|
14
|
+
export declare function latch(pred: Read<boolean>, init: boolean, scope?: Read<boolean>): Read<boolean>;
|
|
15
|
+
/** First false→true edge wins. Returns `{ first, at }` (winner index +
|
|
16
|
+
* recorder-clock time), or undefined until one fires. Sticky once decided. */
|
|
17
|
+
export declare function firstOf(...events: Read<boolean>[]): Read<{
|
|
18
|
+
first: number;
|
|
19
|
+
at: number;
|
|
20
|
+
} | undefined>;
|