bireactive 0.2.1 → 0.2.2

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,8 +1,8 @@
1
1
  # bi-reactive
2
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.
3
+ [npm](https://www.npmjs.com/package/bireactive) · [GitHub](https://github.com/OrionReed/bireactive) · [site](https://orionreed.github.io/bireactive/)
4
+
5
+ A signals-like bidirectional reactive programming system where edges can go both ways. Forward and backward propagation are handled by the engine, with the same set of caveats as regular reactive programming.
6
6
 
7
7
  ## Install
8
8
 
@@ -25,6 +25,12 @@ export declare class Num extends Cell<V> {
25
25
  /** Affine `v ↦ k·v + off`. Invertible iff k ≠ 0; readability alias
26
26
  * for `.scale(k).add(off)`. */
27
27
  affine(k: Val<number>, off: Val<number>): this;
28
+ /** `sin(this)` (radians). Forward lands in [−1, 1]; the inverse is
29
+ * multi-valued, so a write clamps to that domain and returns the
30
+ * pre-image nearest the current source — the drag stays on its branch. */
31
+ sin(): this;
32
+ /** `exp(this)` — bijection on the reals; inverse is the natural log. */
33
+ exp(): this;
28
34
  /** Lossy clamping lens to `[lo, hi]`. PutGet only (a write outside
29
35
  * the range reads back clamped, not as written). */
30
36
  clamp(lo: Val<V>, hi: Val<V>): this;
@@ -11,6 +11,11 @@ export const scale = (a, k) => a * k;
11
11
  export const lerp = (a, b, t) => a + (b - a) * t;
12
12
  export const metric = (a, b) => Math.abs(a - b);
13
13
  export const equals = (a, b) => a === b;
14
+ const TAU = 2 * Math.PI;
15
+ /** Representative of `x + 2πk` nearest `s` (shortest-arc branch pick). */
16
+ const nearestTo = (s, x) => x + TAU * Math.round((s - x) / TAU);
17
+ /** Clamp to the sin/cos domain `[-1, 1]`. */
18
+ const unit = (t) => (t < -1 ? -1 : t > 1 ? 1 : t);
14
19
  const linearImpl = { add, sub, scale };
15
20
  const packImpl = {
16
21
  dim: 1,
@@ -49,6 +54,21 @@ export class Num extends Cell {
49
54
  const of = reader(off);
50
55
  return this.lens(v => v * kf() + of(), n => (n - of()) / kf());
51
56
  }
57
+ /** `sin(this)` (radians). Forward lands in [−1, 1]; the inverse is
58
+ * multi-valued, so a write clamps to that domain and returns the
59
+ * pre-image nearest the current source — the drag stays on its branch. */
60
+ sin() {
61
+ return this.lens(v => Math.sin(v), (target, s) => {
62
+ const p = Math.asin(unit(target));
63
+ const a = nearestTo(s, p);
64
+ const b = nearestTo(s, Math.PI - p);
65
+ return Math.abs(a - s) <= Math.abs(b - s) ? a : b;
66
+ });
67
+ }
68
+ /** `exp(this)` — bijection on the reals; inverse is the natural log. */
69
+ exp() {
70
+ return this.lens(v => Math.exp(v), n => Math.log(n));
71
+ }
52
72
  /** Lossy clamping lens to `[lo, hi]`. PutGet only (a write outside
53
73
  * the range reads back clamped, not as written). */
54
74
  clamp(lo, hi) {
@@ -17,13 +17,15 @@ export declare class Tri extends Cell<V> {
17
17
  constructor(v?: V);
18
18
  /** Kleene negation. Involution; fixed at `"mixed"`. */
19
19
  not(): this;
20
- /** Aggregate over N writable Bools. Read: all-true → `true`,
21
- * all-false → `false`, disagreement `"mixed"`. Write: `true` /
22
- * `false` broadcast to every parent; `"mixed"` is a no-op. */
23
- static allOf(parents: readonly Bool[]): Writable<Tri>;
24
- /** Dual of `allOf` (Kleene OR): any-true `true`, all-false
25
- * `false`, else `"mixed"`. Same broadcast write policy. */
26
- static anyOf(parents: readonly Bool[]): Writable<Tri>;
20
+ /** Aggregate over N writable `Bool` / `Tri` children. Read: all-true →
21
+ * `true`, all-false → `false`, any disagreement (or any child already
22
+ * `"mixed"`) `"mixed"`. Write: `true` / `false` broadcast to every
23
+ * child, recursing through nested aggregates; `"mixed"` is a no-op. */
24
+ static allOf(parents: readonly (Bool | Tri)[]): Writable<Tri>;
25
+ /** Dual of `allOf` (Kleene OR) over `Bool` / `Tri` children: any-true
26
+ * `true`, all-false → `false`, else (or any child `"mixed"`)
27
+ * `"mixed"`. Same broadcast write policy. */
28
+ static anyOf(parents: readonly (Bool | Tri)[]): Writable<Tri>;
27
29
  }
28
30
  /** Writable `Tri`. Strict factory: `Tri.value | Writable<Tri>` in,
29
31
  * `Writable<Tri>` out. Default initial value is `"mixed"`. */
@@ -3,12 +3,6 @@
3
3
  // `Tri.value ∈ { true, false, "mixed" }` — Bool plus an unknown state
4
4
  // fixed under negation. Strong-Kleene AND/OR follow the partial-info
5
5
  // reading (`mixed AND false` → `false`, `mixed AND true` → `mixed`).
6
- //
7
- // Headline use: aggregate N booleans via `Tri.allOf` / `Tri.anyOf`
8
- // (all-agree → that value, disagreement → `"mixed"`). Writing the
9
- // aggregate broadcasts to every parent ("select all" / "deselect all");
10
- // writing `"mixed"` is a no-op. Morally `Maybe<Bool>` — the basis for
11
- // mixed-state checkbox trees and "loading" predicate states.
12
6
  import { Cell } from "../cell.js";
13
7
  const equals = (a, b) => a === b;
14
8
  /** Kleene negation: `true` / `false` swap, `"mixed"` is fixed. */
@@ -40,14 +34,17 @@ export class Tri extends Cell {
40
34
  not() {
41
35
  return this.lens(not, not);
42
36
  }
43
- /** Aggregate over N writable Bools. Read: all-true → `true`,
44
- * all-false → `false`, disagreement `"mixed"`. Write: `true` /
45
- * `false` broadcast to every parent; `"mixed"` is a no-op. */
37
+ /** Aggregate over N writable `Bool` / `Tri` children. Read: all-true →
38
+ * `true`, all-false → `false`, any disagreement (or any child already
39
+ * `"mixed"`) `"mixed"`. Write: `true` / `false` broadcast to every
40
+ * child, recursing through nested aggregates; `"mixed"` is a no-op. */
46
41
  static allOf(parents) {
47
42
  return Tri.lens(parents, (vs) => {
48
43
  let anyT = false;
49
44
  let anyF = false;
50
45
  for (const v of vs) {
46
+ if (v === "mixed")
47
+ return "mixed";
51
48
  if (v)
52
49
  anyT = true;
53
50
  else
@@ -62,13 +59,16 @@ export class Tri extends Cell {
62
59
  return parents.map(() => target);
63
60
  });
64
61
  }
65
- /** Dual of `allOf` (Kleene OR): any-true `true`, all-false
66
- * `false`, else `"mixed"`. Same broadcast write policy. */
62
+ /** Dual of `allOf` (Kleene OR) over `Bool` / `Tri` children: any-true
63
+ * `true`, all-false → `false`, else (or any child `"mixed"`)
64
+ * `"mixed"`. Same broadcast write policy. */
67
65
  static anyOf(parents) {
68
66
  return Tri.lens(parents, (vs) => {
69
67
  let anyT = false;
70
68
  let anyF = false;
71
69
  for (const v of vs) {
70
+ if (v === "mixed")
71
+ return "mixed";
72
72
  if (v)
73
73
  anyT = true;
74
74
  else
@@ -14,6 +14,8 @@ export declare const metric: (a: V, b: V) => number;
14
14
  export declare const equals: (a: V, b: V) => boolean;
15
15
  export declare const normalize: (v: V) => V;
16
16
  export declare const perp: (v: V) => V;
17
+ export declare const rotateAbout: (v: V, p: V, dθ: number) => V;
18
+ export declare const scaleAbout: (v: V, p: V, k: number) => V;
17
19
  /** Tangent point on the circle (radius `r`, centre `c`) from external
18
20
  * point `p`. `side: -1` picks the CCW tangent from `pc`, `+1` the CW
19
21
  * (y-down screen coords flip the visual sense). Returns `c` if `p` is
@@ -32,7 +34,12 @@ export declare class Vec extends Cell<V> {
32
34
  constructor(v?: V);
33
35
  add(b: Val<V>): this;
34
36
  sub(b: Val<V>): this;
35
- scale(k: Val<number>): this;
37
+ /** Uniform scale by `k` about `pivot` (default origin). Inverse scales
38
+ * by `1/k`; exact bijection for `k ≠ 0`. */
39
+ scale(k: Val<number>, pivot?: Val<V>): this;
40
+ /** Rotate by `angle` (radians) about `pivot` (default origin). Inverse
41
+ * rotates by `−angle`; exact bijection. */
42
+ rotate(angle: Val<number>, pivot?: Val<V>): this;
36
43
  offset(dx: Val<number>, dy: Val<number>): this;
37
44
  up(n: Val<number>): this;
38
45
  down(n: Val<number>): this;
@@ -20,6 +20,18 @@ export const normalize = (v) => {
20
20
  return m === 0 ? { x: 0, y: 0 } : { x: v.x / m, y: v.y / m };
21
21
  };
22
22
  export const perp = (v) => ({ x: v.y, y: -v.x });
23
+ export const rotateAbout = (v, p, dθ) => {
24
+ const cos = Math.cos(dθ);
25
+ const sin = Math.sin(dθ);
26
+ const dx = v.x - p.x;
27
+ const dy = v.y - p.y;
28
+ return { x: p.x + cos * dx - sin * dy, y: p.y + sin * dx + cos * dy };
29
+ };
30
+ export const scaleAbout = (v, p, k) => ({
31
+ x: p.x + k * (v.x - p.x),
32
+ y: p.y + k * (v.y - p.y),
33
+ });
34
+ const ORIGIN = { x: 0, y: 0 };
23
35
  /** Tangent point on the circle (radius `r`, centre `c`) from external
24
36
  * point `p`. `side: -1` picks the CCW tangent from `pc`, `+1` the CW
25
37
  * (y-down screen coords flip the visual sense). Returns `c` if `p` is
@@ -49,19 +61,7 @@ const packImpl = {
49
61
  },
50
62
  write: (a, o) => ({ x: a[o], y: a[o + 1] }),
51
63
  };
52
- const pivotalImpl = {
53
- rotateAbout: (v, p, dθ) => {
54
- const cos = Math.cos(dθ);
55
- const sin = Math.sin(dθ);
56
- const dx = v.x - p.x;
57
- const dy = v.y - p.y;
58
- return { x: p.x + cos * dx - sin * dy, y: p.y + sin * dx + cos * dy };
59
- },
60
- scaleAbout: (v, p, k) => ({
61
- x: p.x + k * (v.x - p.x),
62
- y: p.y + k * (v.y - p.y),
63
- }),
64
- };
64
+ const pivotalImpl = { rotateAbout, scaleAbout };
65
65
  export class Vec extends Cell {
66
66
  static traits = {
67
67
  linear: linearImpl,
@@ -94,16 +94,26 @@ export class Vec extends Cell {
94
94
  return { x: n.x + o.x, y: n.y + o.y };
95
95
  });
96
96
  }
97
- scale(k) {
97
+ /** Uniform scale by `k` about `pivot` (default origin). Inverse scales
98
+ * by `1/k`; exact bijection for `k ≠ 0`. */
99
+ scale(k, pivot) {
98
100
  const kf = reader(k);
101
+ const pf = pivot === undefined ? undefined : reader(pivot);
99
102
  return this.lens(v => {
100
103
  const k = kf();
101
- return { x: v.x * k, y: v.y * k };
104
+ return pf ? scaleAbout(v, pf(), k) : { x: v.x * k, y: v.y * k };
102
105
  }, n => {
103
106
  const k = kf();
104
- return { x: n.x / k, y: n.y / k };
107
+ return pf ? scaleAbout(n, pf(), 1 / k) : { x: n.x / k, y: n.y / k };
105
108
  });
106
109
  }
110
+ /** Rotate by `angle` (radians) about `pivot` (default origin). Inverse
111
+ * rotates by `−angle`; exact bijection. */
112
+ rotate(angle, pivot = ORIGIN) {
113
+ const af = reader(angle);
114
+ const pf = reader(pivot);
115
+ return this.lens(v => rotateAbout(v, pf(), af()), n => rotateAbout(n, pf(), -af()));
116
+ }
107
117
  offset(dx, dy) {
108
118
  const xf = reader(dx);
109
119
  const yf = reader(dy);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bireactive",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Bi-directional reactive programming.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",