@msb235/blits 0.14.0 → 0.15.1

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 (187) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +3 -3
  3. package/dist/ahead.d.ts +26 -0
  4. package/dist/ahead.js +264 -0
  5. package/dist/around.d.ts +11 -0
  6. package/dist/around.js +32 -0
  7. package/dist/bare.js +37 -14
  8. package/dist/blank.d.ts +10 -0
  9. package/dist/blank.js +26 -0
  10. package/dist/blend.d.ts +2 -1
  11. package/dist/blend.js +1 -6
  12. package/dist/bound.d.ts +70 -0
  13. package/dist/bound.js +351 -0
  14. package/dist/chain.d.ts +2 -1
  15. package/dist/chain.js +7 -5
  16. package/dist/changes.d.ts +24 -0
  17. package/dist/changes.js +189 -0
  18. package/dist/channels.d.ts +39 -13
  19. package/dist/channels.js +14 -5
  20. package/dist/color.d.ts +12 -10
  21. package/dist/color.js +25 -25
  22. package/dist/columns.d.ts +2 -1
  23. package/dist/columns.js +62 -38
  24. package/dist/contribution.d.ts +30 -0
  25. package/dist/contribution.js +374 -0
  26. package/dist/copyout.d.ts +10 -0
  27. package/dist/copyout.js +69 -24
  28. package/dist/crowd.d.ts +16 -6
  29. package/dist/crowd.js +20 -11
  30. package/dist/cue.d.ts +8 -1
  31. package/dist/cue.js +39 -13
  32. package/dist/cut.d.ts +8 -0
  33. package/dist/cut.js +4 -0
  34. package/dist/delay.d.ts +3 -0
  35. package/dist/delay.js +7 -0
  36. package/dist/drop.d.ts +9 -0
  37. package/dist/drop.js +84 -0
  38. package/dist/due.d.ts +24 -5
  39. package/dist/due.js +114 -34
  40. package/dist/easing.d.ts +6 -4
  41. package/dist/easing.js +7 -3
  42. package/dist/everyone.d.ts +8 -2
  43. package/dist/everyone.js +33 -3
  44. package/dist/fade.d.ts +2 -1
  45. package/dist/fade.js +2 -1
  46. package/dist/fill.d.ts +3 -2
  47. package/dist/fill.js +52 -20
  48. package/dist/fit.d.ts +44 -26
  49. package/dist/fit.js +10 -9
  50. package/dist/fold.d.ts +4 -37
  51. package/dist/fold.js +5 -513
  52. package/dist/gather.d.ts +12 -7
  53. package/dist/gather.js +179 -44
  54. package/dist/gone.js +2 -2
  55. package/dist/grid.d.ts +47 -0
  56. package/dist/grid.js +247 -0
  57. package/dist/handle.d.ts +2 -0
  58. package/dist/handle.js +14 -8
  59. package/dist/held.d.ts +2 -1
  60. package/dist/held.js +2 -1
  61. package/dist/history.d.ts +6 -5
  62. package/dist/history.js +7 -9
  63. package/dist/hosts.js +9 -15
  64. package/dist/index.d.ts +5 -4
  65. package/dist/keyfill.d.ts +1 -1
  66. package/dist/keyfill.js +5 -3
  67. package/dist/keyrows.d.ts +12 -0
  68. package/dist/keyrows.js +99 -12
  69. package/dist/lane.d.ts +38 -11
  70. package/dist/lane.js +46 -11
  71. package/dist/lanes.d.ts +30 -12
  72. package/dist/lanes.js +56 -36
  73. package/dist/leavings.d.ts +2 -2
  74. package/dist/leavings.js +1 -1
  75. package/dist/lives.d.ts +47 -0
  76. package/dist/lives.js +180 -0
  77. package/dist/meet.d.ts +2 -2
  78. package/dist/meet.js +95 -7
  79. package/dist/mixer.d.ts +56 -37
  80. package/dist/mixer.js +57 -171
  81. package/dist/motion.d.ts +14 -118
  82. package/dist/motion.js +16 -8
  83. package/dist/motions.d.ts +62 -155
  84. package/dist/motions.js +56 -370
  85. package/dist/moved.d.ts +1 -1
  86. package/dist/moved.js +16 -7
  87. package/dist/named.d.ts +6 -2
  88. package/dist/named.js +49 -3
  89. package/dist/numbers.d.ts +7 -0
  90. package/dist/numbers.js +33 -0
  91. package/dist/owed.d.ts +2 -0
  92. package/dist/owed.js +4 -0
  93. package/dist/pack.d.ts +2 -1
  94. package/dist/pack.js +2 -3
  95. package/dist/patch.d.ts +7 -3
  96. package/dist/patch.js +6 -2
  97. package/dist/places.d.ts +53 -0
  98. package/dist/places.js +171 -0
  99. package/dist/plot.d.ts +12 -0
  100. package/dist/plot.js +52 -0
  101. package/dist/project.d.ts +4 -4
  102. package/dist/project.js +9 -212
  103. package/dist/pull.js +22 -9
  104. package/dist/qualify.js +93 -31
  105. package/dist/reading.d.ts +6 -0
  106. package/dist/reading.js +5 -0
  107. package/dist/record.d.ts +5 -1
  108. package/dist/record.js +3 -2
  109. package/dist/relink.d.ts +1 -1
  110. package/dist/renumber.d.ts +10 -0
  111. package/dist/renumber.js +75 -0
  112. package/dist/restored.d.ts +2 -2
  113. package/dist/restored.js +1 -1
  114. package/dist/rests.d.ts +9 -0
  115. package/dist/rests.js +76 -0
  116. package/dist/revive.d.ts +3 -2
  117. package/dist/revive.js +2 -2
  118. package/dist/{locus.d.ts → role.d.ts} +10 -9
  119. package/dist/{locus.js → role.js} +3 -3
  120. package/dist/roles.d.ts +10 -0
  121. package/dist/roles.js +158 -0
  122. package/dist/roster.js +3 -3
  123. package/dist/rotation.d.ts +1 -1
  124. package/dist/rotation.js +1 -1
  125. package/dist/rows.js +28 -20
  126. package/dist/sample.d.ts +2 -2
  127. package/dist/sample.js +33 -7
  128. package/dist/seat.d.ts +17 -0
  129. package/dist/seat.js +25 -0
  130. package/dist/seek.js +5 -5
  131. package/dist/setting.d.ts +27 -0
  132. package/dist/setting.js +55 -0
  133. package/dist/shared.d.ts +1 -1
  134. package/dist/shared.js +1 -1
  135. package/dist/signals.d.ts +64 -32
  136. package/dist/signals.js +20 -18
  137. package/dist/stack.d.ts +5 -5
  138. package/dist/stack.js +5 -5
  139. package/dist/standing.d.ts +2 -1
  140. package/dist/standing.js +3 -2
  141. package/dist/store.d.ts +9 -0
  142. package/dist/store.js +24 -0
  143. package/dist/strays.d.ts +2 -1
  144. package/dist/strays.js +23 -15
  145. package/dist/stretch.d.ts +112 -0
  146. package/dist/stretch.js +9 -0
  147. package/dist/subject.d.ts +142 -0
  148. package/dist/subject.js +1 -0
  149. package/dist/tape.d.ts +9 -5
  150. package/dist/tape.js +5 -1
  151. package/dist/then.d.ts +9 -0
  152. package/dist/then.js +57 -0
  153. package/dist/ticker.d.ts +4 -4
  154. package/dist/trace.d.ts +4 -0
  155. package/dist/trace.js +255 -0
  156. package/dist/transport.d.ts +0 -8
  157. package/dist/transport.js +0 -4
  158. package/dist/turns.d.ts +4 -3
  159. package/dist/turns.js +4 -3
  160. package/dist/types/arc.d.ts +12 -11
  161. package/dist/types/channel.d.ts +12 -8
  162. package/dist/types/history.d.ts +35 -19
  163. package/dist/types/mix.d.ts +237 -163
  164. package/dist/types/motion.d.ts +151 -0
  165. package/dist/types/motion.js +1 -0
  166. package/dist/types/patch.d.ts +67 -34
  167. package/dist/types/place.d.ts +51 -36
  168. package/dist/types/trace.d.ts +51 -0
  169. package/dist/types/trace.js +1 -0
  170. package/dist/types/transport.d.ts +20 -18
  171. package/dist/types/voice.d.ts +144 -73
  172. package/dist/types/watch.d.ts +88 -40
  173. package/dist/types.d.ts +2 -0
  174. package/dist/unreached.d.ts +2 -0
  175. package/dist/unreached.js +7 -0
  176. package/dist/voice.d.ts +10 -143
  177. package/dist/voice.js +37 -82
  178. package/dist/warn.d.ts +2 -0
  179. package/dist/warn.js +1 -1
  180. package/dist/watch.d.ts +2 -2
  181. package/dist/watch.js +5 -3
  182. package/dist/wave.d.ts +4 -3
  183. package/dist/wave.js +3 -2
  184. package/dist/weigh.d.ts +7 -1
  185. package/dist/weigh.js +27 -0
  186. package/docs/schema.html +223 -34
  187. package/package.json +2 -2
@@ -0,0 +1,189 @@
1
+ import { absent } from './numbers.js';
2
+ import { reading } from './reading.js';
3
+ import { numbers } from './stretch.js';
4
+ const PENDING = 2 /* RunFlag.PENDING */;
5
+ const OLDER = 4 /* RunFlag.OLDER */;
6
+ /** Where `applied` reads a stretch at the time a change lands; copied out before it returns. */
7
+ let landing = { x: new Float64Array(4), v: new Float64Array(4) };
8
+ /**
9
+ * Queues a retarget, set or push, untimed at the latest frame, for `subject`'s next read past its time.
10
+ * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
11
+ */
12
+ export function change(subject, c) {
13
+ const back = this.owner === null ? Number.NaN : this.owner.revive(this.ownerId, subject);
14
+ const s = this.slot(subject);
15
+ this.check(c.to, this.n);
16
+ this.check(c.x, this.n);
17
+ this.check(c.v, this.n);
18
+ c.made = this.owner === null ? Number.NaN : this.owner.now();
19
+ c.seq = this.owner === null ? Number.NaN : this.owner.seq();
20
+ c.early = this.owner?.early() === true;
21
+ if (c.at === undefined) {
22
+ const at = this.frame(subject);
23
+ if (!Number.isNaN(at))
24
+ c.at = at;
25
+ else if (!Number.isNaN(back))
26
+ c.at = back;
27
+ }
28
+ const list = this.pending?.[s];
29
+ if (list === undefined) {
30
+ if (this.pending === null)
31
+ this.pending = [undefined];
32
+ this.pending[s] = [c];
33
+ this.flag(s, PENDING, true);
34
+ }
35
+ else if (c.at === undefined)
36
+ list.push(c);
37
+ else
38
+ this.queue(list, c, c.at);
39
+ reading.moved++;
40
+ const made = { at: c.at, to: c.to, x: c.x, v: c.v };
41
+ this.owner?.changed(this.ownerId, this, subject, made);
42
+ }
43
+ /** Files a change with a time after every one due by then, ahead of those still waiting for one. */
44
+ export function queue(list, c, at) {
45
+ let i = list.length;
46
+ while (i > 0) {
47
+ const before = list[i - 1].at;
48
+ if (before !== undefined && before <= at)
49
+ break;
50
+ i--;
51
+ }
52
+ list.splice(i, 0, c);
53
+ }
54
+ /** Every change due by `t` applied to a copy of the stretch playing then, committing nothing. */
55
+ export function peek(s, t, xo, vo) {
56
+ let seg = this.playing(s, t);
57
+ const list = this.dueTo(s);
58
+ if (list !== undefined)
59
+ for (const change of list) {
60
+ const a = change.at;
61
+ if (a === undefined || !(a <= t))
62
+ break;
63
+ if (a >= seg.at)
64
+ seg = this.applied(this.numbers.subject(s), seg, change, a);
65
+ }
66
+ this.evaluateSegment(seg, t, xo, vo);
67
+ }
68
+ /** Subject `s`'s changes waiting, with those a read ahead's tape made filed among them by time. */
69
+ export function dueTo(s) {
70
+ const own = this.pending?.[s];
71
+ const ahead = reading.ahead?.get(this);
72
+ if (ahead === undefined)
73
+ return own;
74
+ const subject = this.numbers.subject(s);
75
+ const made = subject === absent ? undefined : ahead.get(subject);
76
+ if (made === undefined)
77
+ return own;
78
+ if (own === undefined)
79
+ return made;
80
+ // The patch's own were made first, and a stable sort keeps them first at one time.
81
+ return [...own.filter((c) => c.at !== undefined), ...made].sort((a, b) => a.at - b.at);
82
+ }
83
+ /**
84
+ * Takes back every change made after frame `seq`: each subject plays from its earliest stretch
85
+ * kept, with the changes made by then queued to be applied again.
86
+ */
87
+ export function rewind(seq) {
88
+ this.unrelease(seq);
89
+ for (let s = 0; s < this.numbers.size; s++)
90
+ if (this.numbers.subject(s) !== absent)
91
+ this.cut(s, seq);
92
+ }
93
+ export function cut(s, seq) {
94
+ const older = this.older?.[s];
95
+ const pending = this.pending?.[s];
96
+ const current = this.latest(s);
97
+ const stretches = older === undefined ? [current] : [...older, current];
98
+ const kept = [];
99
+ let cut = false;
100
+ for (const c of [...stretches.slice(1).map((seg) => seg.change), ...(pending ?? [])])
101
+ if (c === undefined || c.seq === undefined || !(c.seq > seq)) {
102
+ if (c !== undefined)
103
+ kept.push(c);
104
+ }
105
+ else
106
+ cut = true;
107
+ if (!cut)
108
+ return;
109
+ if (older !== undefined) {
110
+ this.older[s] = undefined;
111
+ this.flag(s, OLDER, false);
112
+ }
113
+ this.write(s, stretches[0]);
114
+ // Applied again in the order they take effect; one never stamped waits for the next read.
115
+ const timed = kept
116
+ .filter((c) => c.at !== undefined)
117
+ .sort((a, b) => a.at - b.at);
118
+ const list = [...timed, ...kept.filter((c) => c.at === undefined)];
119
+ if (list.length === 0) {
120
+ if (pending !== undefined) {
121
+ this.pending[s] = undefined;
122
+ this.flag(s, PENDING, false);
123
+ }
124
+ return;
125
+ }
126
+ if (this.pending === null)
127
+ this.pending = [undefined];
128
+ this.pending[s] = list;
129
+ this.flag(s, PENDING, true);
130
+ }
131
+ /** The stretch `change` releases from `seg` at `at`, for `subject` or one no longer numbered. */
132
+ export function applied(subject, seg, change, at) {
133
+ const n = this.n;
134
+ if (landing.x.length < n)
135
+ landing = { x: new Float64Array(n), v: new Float64Array(n) };
136
+ const { x, v } = landing;
137
+ this.evaluateSegment(seg, at, x, v);
138
+ const xs = change.x ?? numbers(x, 0, n);
139
+ const vs = change.v ?? numbers(v, 0, n);
140
+ const known = subject === absent ? undefined : subject;
141
+ const put = change.x !== undefined && change.v === undefined;
142
+ const to = this.shape.aim(xs, vs, change.to ?? seg.to, known, put);
143
+ const ms = subject === absent ? seg.ms : (this.shape.ms?.(known) ?? 0);
144
+ return { at, ms, x0: xs, v0: vs, to, change };
145
+ }
146
+ export function commit(s, t) {
147
+ const list = this.pending[s];
148
+ // The first read of a subject no frame had met: those waiting for a time take this one.
149
+ if (list[list.length - 1].at === undefined) {
150
+ let i = list.length;
151
+ while (i > 0 && list[i - 1].at === undefined)
152
+ i--;
153
+ for (const c of list.splice(i)) {
154
+ c.at = t;
155
+ this.queue(list, c, t);
156
+ }
157
+ }
158
+ while (list.length > 0) {
159
+ const change = list[0];
160
+ const a = change.at;
161
+ if (a > t)
162
+ break;
163
+ list.shift();
164
+ this.insert(s, this.applied(this.numbers.subject(s), this.playing(s, a), change, a));
165
+ }
166
+ if (list.length === 0) {
167
+ this.pending[s] = undefined;
168
+ this.flag(s, PENDING, false);
169
+ }
170
+ }
171
+ export function insert(s, seg) {
172
+ let list = this.older?.[s];
173
+ if (list === undefined) {
174
+ list = [];
175
+ if (this.older === null)
176
+ this.older = [undefined];
177
+ this.older[s] = list;
178
+ this.flag(s, OLDER, true);
179
+ }
180
+ if (seg.at >= this.runs[this.base(s)]) {
181
+ list.push(this.latest(s));
182
+ this.write(s, seg);
183
+ return;
184
+ }
185
+ let i = list.length;
186
+ while (i > 0 && list[i - 1].at > seg.at)
187
+ i--;
188
+ list.splice(i, 0, seg);
189
+ }
@@ -26,6 +26,11 @@ export interface Numeric {
26
26
  axes: number;
27
27
  /** An angle's full turn: each value is taken the short way round before it folds. */
28
28
  turn?: number;
29
+ /**
30
+ * An `'own'` channel whose fold is this plain arithmetic, its lerp alone its own: a lane folds
31
+ * it as the op and lerps it through the channel. A copy that predates it folds it as `'own'`.
32
+ */
33
+ folds?: 'sum';
29
34
  }
30
35
  /**
31
36
  * The stock arithmetic of a channel `sum`, `mul`, `max` or `vec` made, or `vouch` was given, keyed
@@ -55,49 +60,71 @@ export interface NumberOptions {
55
60
  bounds?: readonly [min: number, max: number];
56
61
  }
57
62
  /**
58
- * Position axes, rotation, crawl, yaw, pitch, offset.
63
+ * A number whose contributions add, resting at 0.
64
+ *
65
+ * A weight scales a contribution toward 0, so two voices each moving `x` by 10 at half weight move it
66
+ * 10 in all. For position axes, rotation, crawl, yaw, pitch, offset.
59
67
  *
60
68
  * @category channel
61
69
  */
62
70
  export declare function sum(opts?: NumberOptions): Channel<number>;
63
71
  /**
64
- * Gain, scale, opacity, hold.
72
+ * A number whose contributions multiply, resting at 1.
73
+ *
74
+ * A weight fades a contribution toward 1 rather than 0, so 0.5 at half weight multiplies by 0.75, and
75
+ * at weight 0 it leaves the value alone. For gain, scale, opacity, hold.
65
76
  *
66
77
  * @category channel
67
78
  */
68
79
  export declare function mul(opts?: NumberOptions): Channel<number>;
69
80
  /**
70
- * Dark, aperture.
81
+ * A number where the largest contribution wins, resting at 0.
82
+ *
83
+ * A weight scales a contribution toward 0 before the comparison, so a fading voice gives way once
84
+ * another's value passes its scaled one. For dark, aperture.
71
85
  *
72
86
  * @category channel
73
87
  */
74
88
  export declare function max(opts?: NumberOptions): Channel<number>;
75
89
  /**
76
- * A channel with no rest: the last contribution to pass wins, and a weight can only gate it. The
90
+ * What `last` takes.
91
+ *
92
+ * @category channel
93
+ */
94
+ export interface LastOptions<V> {
95
+ /** How to interpolate between two values. Default: step at the midpoint. */
96
+ lerp?: (a: V, b: V, u: number) => V;
97
+ }
98
+ /**
99
+ * A channel with no rest, where the last contribution to pass wins. A weight can only gate it. The
77
100
  * default `lerp` steps at the midpoint, which is all a value with no arithmetic can promise; a
78
101
  * consumer with a real interpolation passes its own, as `hex` does.
79
102
  *
80
103
  * @category channel
81
104
  */
82
- export declare function last<V>(opts?: {
83
- lerp?: (a: V, b: V, u: number) => V;
84
- }): Channel<V>;
105
+ export declare function last<V>(opts?: LastOptions<V>): Channel<V>;
85
106
  /**
86
107
  * `n` copies of `v` in an array the engine stores as doubles from the start, so a copy of it, and
87
108
  * every fraction written into that copy, keeps one element kind: a pose's arrays then look alike to
88
109
  * every fold, whichever path made them.
89
110
  */
90
111
  export declare function doubles(n: number, v: number): number[];
91
- type Tuple<N extends number, T extends number[] = []> = T['length'] extends N ? T : Tuple<N, [...T, number]>;
92
112
  /**
93
- * `N` numbers: a tuple where `N` is a literal, as `Vec<3>` is `[number, number, number]`, and
94
- * `number[]` where it is only known to be a number.
113
+ * A tuple of `N` numbers, as `NumberTuple<3>` is `[number, number, number]`. `T` is the tuple built
114
+ * so far; leave it out.
115
+ *
116
+ * @category channel
117
+ */
118
+ export type NumberTuple<N extends number, T extends number[] = []> = T['length'] extends N ? T : NumberTuple<N, [...T, number]>;
119
+ /**
120
+ * `N` numbers, as a tuple where `N` is a literal. `Vec<3>` is `[number, number, number]`, and a
121
+ * `Vec` is `number[]` where `N` is only known to be a number.
95
122
  *
96
123
  * @category channel
97
124
  */
98
- export type Vec<N extends number> = number extends N ? number[] : Tuple<N>;
125
+ export type Vec<N extends number> = number extends N ? number[] : NumberTuple<N>;
99
126
  /**
100
- * Vec3 position, premultiplied light: one channel's arithmetic applied down an axis list.
127
+ * One channel's arithmetic applied down an axis list. For a Vec3 position, or premultiplied light.
101
128
  *
102
129
  * @category channel
103
130
  */
@@ -108,4 +135,3 @@ export declare function vec<N extends number>(n: N, of: Channel<number>): Channe
108
135
  * @category channel
109
136
  */
110
137
  export declare function kit<O>(channels: Kit<O>): Kit<O>;
111
- export {};
package/dist/channels.js CHANGED
@@ -67,7 +67,10 @@ export function scalarOp(channel) {
67
67
  /** Bounds change how a value folds at its edges, so they are part of the kind. */
68
68
  const kindOf = (name, bounds) => bounds === undefined ? name : `${name}[${bounds[0]}, ${bounds[1]}]`;
69
69
  /**
70
- * Position axes, rotation, crawl, yaw, pitch, offset.
70
+ * A number whose contributions add, resting at 0.
71
+ *
72
+ * A weight scales a contribution toward 0, so two voices each moving `x` by 10 at half weight move it
73
+ * 10 in all. For position axes, rotation, crawl, yaw, pitch, offset.
71
74
  *
72
75
  * @category channel
73
76
  */
@@ -84,7 +87,10 @@ export function sum(opts) {
84
87
  return channel;
85
88
  }
86
89
  /**
87
- * Gain, scale, opacity, hold.
90
+ * A number whose contributions multiply, resting at 1.
91
+ *
92
+ * A weight fades a contribution toward 1 rather than 0, so 0.5 at half weight multiplies by 0.75, and
93
+ * at weight 0 it leaves the value alone. For gain, scale, opacity, hold.
88
94
  *
89
95
  * @category channel
90
96
  */
@@ -101,7 +107,10 @@ export function mul(opts) {
101
107
  return channel;
102
108
  }
103
109
  /**
104
- * Dark, aperture.
110
+ * A number where the largest contribution wins, resting at 0.
111
+ *
112
+ * A weight scales a contribution toward 0 before the comparison, so a fading voice gives way once
113
+ * another's value passes its scaled one. For dark, aperture.
105
114
  *
106
115
  * @category channel
107
116
  */
@@ -118,7 +127,7 @@ export function max(opts) {
118
127
  return channel;
119
128
  }
120
129
  /**
121
- * A channel with no rest: the last contribution to pass wins, and a weight can only gate it. The
130
+ * A channel with no rest, where the last contribution to pass wins. A weight can only gate it. The
122
131
  * default `lerp` steps at the midpoint, which is all a value with no arithmetic can promise; a
123
132
  * consumer with a real interpolation passes its own, as `hex` does.
124
133
  *
@@ -148,7 +157,7 @@ export function doubles(n, v) {
148
157
  return a;
149
158
  }
150
159
  /**
151
- * Vec3 position, premultiplied light: one channel's arithmetic applied down an axis list.
160
+ * One channel's arithmetic applied down an axis list. For a Vec3 position, or premultiplied light.
152
161
  *
153
162
  * @category channel
154
163
  */
package/dist/color.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import type { Channel } from './types.js';
2
2
  /**
3
- * OKLCH, hue the short way round, so a crossfade between hues keeps its lightness and saturation
4
- * instead of dipping through a muddy middle. A mix that leaves sRGB's gamut is clipped per channel.
3
+ * Crossfades two hex colors in OKLCH, hue the short way round. A crossfade between hues then keeps
4
+ * its lightness and saturation instead of dipping through a muddy middle. A mix that leaves sRGB's
5
+ * gamut is clipped per channel.
5
6
  *
6
7
  * @category channel
7
8
  */
8
9
  export declare function mixHex(a: number, b: number, u: number): number;
9
10
  /**
10
- * How a `color` channel interpolates two colors: straight across OKLab, or in OKLCH with hue the
11
+ * How a `color` channel interpolates two colors. Straight across OKLab, or in OKLCH with hue the
11
12
  * short way round, as `mixHex` does.
12
13
  *
13
14
  * @category channel
@@ -21,29 +22,30 @@ export interface ColorOptions {
21
22
  * `toHex` divides it out and lays it over what is under it. `color(last())` replaces, the last
22
23
  * voice to pass winning, at the coverage it was authored with. `lerp` picks how two colors
23
24
  * interpolate: OKLab, the default, crosses through gray between opposite hues, and `'oklch'`
24
- * goes round the hue instead, off lanes.
25
+ * goes round the hue instead.
25
26
  *
26
27
  * @category channel
27
28
  */
28
29
  export declare function color(opts?: ColorOptions): Channel<number[]>;
29
30
  export declare function color(of: Channel<number> | Channel<unknown>, opts?: ColorOptions): Channel<number[]>;
30
31
  /**
31
- * 0xrrggbb as a `color` value: OKLab `[L, a, b]` at full coverage. Convert keyframe stops once
32
- * when authoring them, and call it outside the hot path in an `fn` patch.
32
+ * Converts 0xrrggbb to a `color` value. The value is OKLab `[L, a, b]` at full coverage. Convert
33
+ * keyframe stops once when authoring them, and call it outside the hot path in an `fn` patch.
33
34
  *
34
35
  * @category channel
35
36
  */
36
37
  export declare function oklab(rgb: number): number[];
37
38
  /**
38
- * A `color` pose as 0xrrggbb: its color divided out of its coverage, the coverage capped at 1
39
- * and laid over `under` in OKLab, then clipped to sRGB per channel. No coverage gives `under`.
39
+ * A `color` pose as 0xrrggbb, laid over `under`. Its color is divided out of its coverage, the
40
+ * coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per channel. No
41
+ * coverage gives `under`.
40
42
  *
41
43
  * @category channel
42
44
  */
43
45
  export declare function toHex(pose: ArrayLike<number>, under?: number): number;
44
46
  /**
45
- * A `color` pose as a CSS `oklab()` color with its coverage, capped at 1, as the alpha, for a
46
- * host that lets the browser lay it over what is under it.
47
+ * A `color` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is capped at 1.
48
+ * For a host that lets the browser lay it over what is under it.
47
49
  *
48
50
  * @category channel
49
51
  */
package/dist/color.js CHANGED
@@ -82,8 +82,9 @@ function fromOklch(L, C, h) {
82
82
  return fromLab(L, C * Math.cos(h), C * Math.sin(h));
83
83
  }
84
84
  /**
85
- * OKLCH, hue the short way round, so a crossfade between hues keeps its lightness and saturation
86
- * instead of dipping through a muddy middle. A mix that leaves sRGB's gamut is clipped per channel.
85
+ * Crossfades two hex colors in OKLCH, hue the short way round. A crossfade between hues then keeps
86
+ * its lightness and saturation instead of dipping through a muddy middle. A mix that leaves sRGB's
87
+ * gamut is clipped per channel.
87
88
  *
88
89
  * @category channel
89
90
  */
@@ -98,14 +99,19 @@ export function mixHex(a, b, u) {
98
99
  const ah = slotH[sa];
99
100
  const sb = oklch(b);
100
101
  const bc = slotC[sb];
101
- const from = ac < GRAY ? slotH[sb] : ah;
102
- const to = bc < GRAY ? ah : slotH[sb];
102
+ const h = hueAt(ah, ac, slotH[sb], bc, u);
103
+ return fromOklch(mix(al, slotL[sb], u), mix(ac, bc, u), h);
104
+ }
105
+ /** The hue `u` of the way from `ah` to `bh` the short way round, a gray end taking the other's. */
106
+ function hueAt(ah, ac, bh, bc, u) {
107
+ const from = ac < GRAY ? bh : ah;
108
+ const to = bc < GRAY ? ah : bh;
103
109
  let turn = to - from;
104
110
  if (turn > Math.PI)
105
111
  turn -= 2 * Math.PI;
106
112
  else if (turn < -Math.PI)
107
113
  turn += 2 * Math.PI;
108
- return fromOklch(mix(al, slotL[sb], u), mix(ac, bc, u), from + turn * u);
114
+ return from + turn * u;
109
115
  }
110
116
  /** Two OKLab colors with coverage, interpolated in OKLCH: color by `u`, coverage by `u` alongside. */
111
117
  function lerpLch(x, y, u) {
@@ -126,17 +132,8 @@ function lerpLch(x, y, u) {
126
132
  const toB = yw > 0 ? yB : xB;
127
133
  const xc = Math.sqrt(xA * xA + xB * xB);
128
134
  const yc = Math.sqrt(toA * toA + toB * toB);
129
- const xh = Math.atan2(xB, xA);
130
- const yh = Math.atan2(toB, toA);
131
- const from = xc < GRAY ? yh : xh;
132
- const to = yc < GRAY ? xh : yh;
133
- let turn = to - from;
134
- if (turn > Math.PI)
135
- turn -= 2 * Math.PI;
136
- else if (turn < -Math.PI)
137
- turn += 2 * Math.PI;
138
135
  const C = mix(xc, yc, u);
139
- const h = from + turn * u;
136
+ const h = hueAt(Math.atan2(xB, xA), xc, Math.atan2(toB, toA), yc, u);
140
137
  return [mix(xL, toL, u) * w, C * Math.cos(h) * w, C * Math.sin(h) * w, w];
141
138
  }
142
139
  export function color(ofOrOpts, maybe) {
@@ -149,8 +146,11 @@ export function color(ofOrOpts, maybe) {
149
146
  (stock?.op === 'sum' && stock.turn === undefined && of.bounds === undefined)) {
150
147
  const channel = vec(4, sum());
151
148
  channel.kind = `color(sum${space})`;
152
- // A lane interpolates a stock channel straight across, so a lerp of its own takes it off them.
153
- return round ? { ...channel, lerp: lerpLch } : channel;
149
+ if (!round)
150
+ return channel;
151
+ const turning = { ...channel, lerp: lerpLch };
152
+ vouch(turning, { op: 'own', axes: 4, folds: 'sum' });
153
+ return turning;
154
154
  }
155
155
  if (of.kind === 'last') {
156
156
  const channel = {
@@ -159,15 +159,14 @@ export function color(ofOrOpts, maybe) {
159
159
  lerp: round ? lerpLch : vec(4, sum()).lerp,
160
160
  };
161
161
  widen(channel, 4);
162
- if (!round)
163
- vouch(channel, { op: 'last', axes: 4 });
162
+ vouch(channel, { op: 'last', axes: 4 });
164
163
  return channel;
165
164
  }
166
165
  throw new Error(`color takes sum() to average or last() to replace, not ${of.kind ?? 'a channel of its own'}`);
167
166
  }
168
167
  /**
169
- * 0xrrggbb as a `color` value: OKLab `[L, a, b]` at full coverage. Convert keyframe stops once
170
- * when authoring them, and call it outside the hot path in an `fn` patch.
168
+ * Converts 0xrrggbb to a `color` value. The value is OKLab `[L, a, b]` at full coverage. Convert
169
+ * keyframe stops once when authoring them, and call it outside the hot path in an `fn` patch.
171
170
  *
172
171
  * @category channel
173
172
  */
@@ -176,8 +175,9 @@ export function oklab(rgb) {
176
175
  return [LAB[0], LAB[1], LAB[2], 1];
177
176
  }
178
177
  /**
179
- * A `color` pose as 0xrrggbb: its color divided out of its coverage, the coverage capped at 1
180
- * and laid over `under` in OKLab, then clipped to sRGB per channel. No coverage gives `under`.
178
+ * A `color` pose as 0xrrggbb, laid over `under`. Its color is divided out of its coverage, the
179
+ * coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per channel. No
180
+ * coverage gives `under`.
181
181
  *
182
182
  * @category channel
183
183
  */
@@ -195,8 +195,8 @@ export function toHex(pose, under = 0x000000) {
195
195
  return fromLab(mix(LAB[0], L, w), mix(LAB[1], A, w), mix(LAB[2], B, w));
196
196
  }
197
197
  /**
198
- * A `color` pose as a CSS `oklab()` color with its coverage, capped at 1, as the alpha, for a
199
- * host that lets the browser lay it over what is under it.
198
+ * A `color` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is capped at 1.
199
+ * For a host that lets the browser lay it over what is under it.
200
200
  *
201
201
  * @category channel
202
202
  */
package/dist/columns.d.ts CHANGED
@@ -22,7 +22,8 @@ export declare function pullRun<I, O>(lanes: Lanes<I, O>, slots: Int32Array, lis
22
22
  * Queues a subject's values for each column at subject `n`'s places, for a `pull` that reads
23
23
  * from the lanes while every voice is on one; `flush` writes what is queued, column by column. A
24
24
  * channel no lane holds is at rest. Anything queued is written before the lanes fill again.
25
+ * `slots` holds each row's lane slot, `slot` among them.
25
26
  */
26
- export declare function writeLater<I, O>(lanes: Lanes<I, O>, slot: number, columns: readonly Column[], n: number): void;
27
+ export declare function writeLater<I, O>(lanes: Lanes<I, O>, slot: number, columns: readonly Column[], n: number, slots: Int32Array): void;
27
28
  /** Writes every queued run into its columns: a channel's values as a block, a rest as a fill. */
28
29
  export declare function flush<I, O>(lanes: Lanes<I, O>): void;
package/dist/columns.js CHANGED
@@ -22,16 +22,12 @@ export function pullRun(lanes, slots, list, was, since, from, columns, now, vers
22
22
  const frame = lanes.frameProbes;
23
23
  const subjects = lanes.subjects;
24
24
  const unlinked = lanes.unlinked;
25
+ const holds = lanes.holds;
26
+ // Nothing in `pullRun` makes a subject owe a voice, so one with nobody owing need not ask.
27
+ const owing = lanes.owed.any();
25
28
  const current = since === Number.POSITIVE_INFINITY;
26
- if (columns !== lanes.queuedFor) {
27
- flush(lanes);
28
- lanes.queuedFor = columns;
29
- }
29
+ queueFor(lanes, columns, slots);
30
30
  room(lanes, lanes.queued + list.length - from);
31
- const qSlots = lanes.queueSlots;
32
- const qRows = lanes.queueRows;
33
- const qCounts = lanes.queueCounts;
34
- let k = lanes.queued;
35
31
  let n = from;
36
32
  // The run being gathered: its first slot and row, and how far it has reached.
37
33
  let first = -1;
@@ -47,33 +43,25 @@ export function pullRun(lanes, slots, list, was, since, from, columns, now, vers
47
43
  per[o + 0 /* Per.FILLED */] >= busy ||
48
44
  per[o + 2 /* Per.IDLE */] !== 0 ||
49
45
  per[o + 1 /* Per.SEEN */] < wide ||
50
- lanes.owes(slot))
46
+ (owing && lanes.owes(slot)))
51
47
  break;
52
48
  if (!(per[o + 3 /* Per.LANE_PROBE */] > frame ||
53
49
  per[o + 4 /* Per.GENERAL_PROBE */] > frame))
54
50
  lanes.distinct++;
55
51
  per[o + 3 /* Per.LANE_PROBE */] = ++lanes.probes;
56
52
  per[o + 5 /* Per.LANE_FILL */] = fills;
57
- subjects[slot] = subject;
53
+ if (holds)
54
+ subjects[slot] = subject;
58
55
  if (slot !== first + (n - start)) {
59
- if (first >= 0) {
60
- qSlots[k] = first;
61
- qRows[k] = start;
62
- qCounts[k] = n - start;
63
- k++;
64
- }
56
+ if (first >= 0)
57
+ add(lanes, first, start, n - start);
65
58
  first = slot;
66
59
  start = n;
67
60
  }
68
61
  }
69
- if (first >= 0) {
70
- qSlots[k] = first;
71
- qRows[k] = start;
72
- qCounts[k] = n - start;
73
- k++;
74
- }
75
- lanes.queued = k;
76
- if (n > from)
62
+ if (first >= 0)
63
+ add(lanes, first, start, n - start);
64
+ if (n > from && holds)
77
65
  lanes.holding = true;
78
66
  return n;
79
67
  }
@@ -81,24 +69,43 @@ export function pullRun(lanes, slots, list, was, since, from, columns, now, vers
81
69
  * Queues a subject's values for each column at subject `n`'s places, for a `pull` that reads
82
70
  * from the lanes while every voice is on one; `flush` writes what is queued, column by column. A
83
71
  * channel no lane holds is at rest. Anything queued is written before the lanes fill again.
72
+ * `slots` holds each row's lane slot, `slot` among them.
84
73
  */
85
- export function writeLater(lanes, slot, columns, n) {
86
- queue(lanes, columns, slot, n, 1);
74
+ export function writeLater(lanes, slot, columns, n, slots) {
75
+ queueFor(lanes, columns, slots);
76
+ add(lanes, slot, n, 1);
87
77
  }
88
- /** Queues `count` subjects from lane slot `slot` for rows from `row`, one apart in both. */
89
- function queue(lanes, columns, slot, row, count) {
90
- if (columns !== lanes.queuedFor) {
91
- flush(lanes);
92
- lanes.queuedFor = columns;
93
- }
78
+ /** Writes what is queued for other columns or another array of slots before queuing for these. */
79
+ function queueFor(lanes, columns, slots) {
80
+ if (columns === lanes.queuedFor && slots === lanes.queueFrom)
81
+ return;
82
+ flush(lanes);
83
+ lanes.queuedFor = columns;
84
+ lanes.queueFrom = slots;
85
+ }
86
+ /**
87
+ * The shortest run `flush` copies as a block from consecutive slots. A shorter one, as a list kept
88
+ * dense by swap-remove leaves its slots, joins the rows before it in one run whose slots `flush`
89
+ * reads row by row, so a run's cost is paid once a stretch of rows rather than once a few.
90
+ */
91
+ const BLOCK = 16;
92
+ /** Queues `count` rows from `row`, whose slots follow one apart from `slot`. */
93
+ function add(lanes, slot, row, count) {
94
94
  const k = lanes.queued;
95
95
  if (k > 0) {
96
96
  const last = k - 1;
97
97
  const had = lanes.queueCounts[last];
98
- if (slot === lanes.queueSlots[last] + had &&
99
- row === lanes.queueRows[last] + had) {
100
- lanes.queueCounts[last] = had + count;
101
- return;
98
+ if (row === lanes.queueRows[last] + had) {
99
+ const first = lanes.queueSlots[last];
100
+ if (first >= 0 && slot === first + had) {
101
+ lanes.queueCounts[last] = had + count;
102
+ return;
103
+ }
104
+ if (count < BLOCK && (first < 0 || had < BLOCK)) {
105
+ lanes.queueSlots[last] = -1;
106
+ lanes.queueCounts[last] = had + count;
107
+ return;
108
+ }
102
109
  }
103
110
  }
104
111
  room(lanes, k + 1);
@@ -169,16 +176,33 @@ function rests(lanes, c, runs) {
169
176
  }
170
177
  /** The longest run of values `copies` copies itself: `set` takes a view, an object a run. */
171
178
  const VIEWED = 256;
172
- /** Copies a laned channel's values for every queued run into its rows. */
179
+ /** Copies a laned channel's values for every queued run into its rows, a block or row by row. */
173
180
  function copies(lanes, c, values, runs) {
174
181
  const slots = lanes.queueSlots;
175
182
  const rows = lanes.queueRows;
176
183
  const counts = lanes.queueCounts;
177
184
  const out = c.out;
178
185
  const axes = c.axes;
186
+ const from = lanes.queueFrom;
179
187
  for (let r = 0; r < runs; r++) {
188
+ const first = slots[r];
189
+ if (first < 0) {
190
+ const row = rows[r];
191
+ const end = row + counts[r];
192
+ if (axes === 1)
193
+ for (let i = row; i < end; i++)
194
+ out[i] = values[from[i]];
195
+ else
196
+ for (let i = row; i < end; i++) {
197
+ const at = i * axes;
198
+ const base = from[i] * axes;
199
+ for (let a = 0; a < axes; a++)
200
+ out[at + a] = values[base + a];
201
+ }
202
+ continue;
203
+ }
180
204
  const at = rows[r] * axes;
181
- const base = slots[r] * axes;
205
+ const base = first * axes;
182
206
  const span = counts[r] * axes;
183
207
  if (span > VIEWED)
184
208
  out.set(values.subarray(base, base + span), at);