@msb235/blits 0.15.1 → 0.16.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 (83) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +11 -13
  4. package/dist/chain.js +2 -3
  5. package/dist/clock.d.ts +11 -1
  6. package/dist/clock.js +28 -9
  7. package/dist/color.d.ts +36 -14
  8. package/dist/color.js +186 -58
  9. package/dist/columns.d.ts +1 -11
  10. package/dist/columns.js +1 -6
  11. package/dist/contribution.js +11 -4
  12. package/dist/crowd.d.ts +0 -6
  13. package/dist/crowd.js +0 -11
  14. package/dist/cue.js +19 -7
  15. package/dist/due.d.ts +18 -0
  16. package/dist/due.js +101 -2
  17. package/dist/everyone.d.ts +17 -6
  18. package/dist/everyone.js +17 -8
  19. package/dist/fade.d.ts +22 -1
  20. package/dist/fade.js +81 -12
  21. package/dist/fill.js +2 -1
  22. package/dist/fit.js +8 -0
  23. package/dist/fold.js +4 -7
  24. package/dist/gone.js +1 -0
  25. package/dist/handle.d.ts +1 -1
  26. package/dist/handle.js +3 -1
  27. package/dist/held.d.ts +2 -1
  28. package/dist/held.js +17 -10
  29. package/dist/hosts.d.ts +7 -0
  30. package/dist/hosts.js +23 -6
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/lane.d.ts +2 -0
  34. package/dist/lane.js +2 -0
  35. package/dist/lanes.d.ts +26 -1
  36. package/dist/lanes.js +9 -1
  37. package/dist/lch.d.ts +23 -0
  38. package/dist/lch.js +126 -0
  39. package/dist/meet.js +9 -0
  40. package/dist/mixer.d.ts +6 -1
  41. package/dist/mixer.js +5 -0
  42. package/dist/motion.js +6 -6
  43. package/dist/motions.d.ts +4 -1
  44. package/dist/motions.js +16 -2
  45. package/dist/move.js +311 -79
  46. package/dist/origin.d.ts +2 -0
  47. package/dist/origin.js +4 -0
  48. package/dist/owner.d.ts +7 -2
  49. package/dist/owner.js +17 -5
  50. package/dist/paging.d.ts +5 -1
  51. package/dist/paging.js +13 -3
  52. package/dist/patch.js +24 -4
  53. package/dist/place.js +67 -31
  54. package/dist/project.d.ts +5 -2
  55. package/dist/project.js +9 -11
  56. package/dist/pull.d.ts +11 -0
  57. package/dist/pull.js +10 -6
  58. package/dist/qualify.js +7 -2
  59. package/dist/rests.js +9 -1
  60. package/dist/revive.d.ts +11 -1
  61. package/dist/revive.js +7 -0
  62. package/dist/roles.js +6 -5
  63. package/dist/roster.d.ts +6 -0
  64. package/dist/roster.js +8 -1
  65. package/dist/rows.js +4 -1
  66. package/dist/sample.js +2 -1
  67. package/dist/seat.d.ts +5 -1
  68. package/dist/seat.js +5 -1
  69. package/dist/seek.js +6 -16
  70. package/dist/shared.d.ts +1 -1
  71. package/dist/shared.js +1 -1
  72. package/dist/spans.d.ts +13 -4
  73. package/dist/spans.js +125 -32
  74. package/dist/touch.js +1 -2
  75. package/dist/trace.js +7 -2
  76. package/dist/turns.js +2 -1
  77. package/dist/types/transport.d.ts +2 -1
  78. package/dist/types/voice.d.ts +3 -2
  79. package/dist/voice.d.ts +59 -2
  80. package/dist/voice.js +76 -2
  81. package/dist/weigh.js +1 -1
  82. package/docs/schema.html +38 -12
  83. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,190 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
4
4
  version and everything else the patch. Each release lists its changes as **Breaking**, **Added** and
5
5
  **Fixed**, and the release workflow refuses a tag with no section here.
6
6
 
7
+ ## 0.16.1
8
+
9
+ ### Fixed
10
+
11
+ - A child anchored to a sibling under an owner at rate 0 plays where its anchor puts it when the
12
+ owner is sought forward past both. Its anchor's time went through mix time on a clock standing
13
+ still, which maps every time it has passed to the seek's moment, so the child was placed at the
14
+ time sought plus its offset and never played.
15
+ - `inert` reads false while a motion voice has a subject it met inside its `stagger` and has not
16
+ read since, which moves once it is probed. It read true, so a host that stops on `inert` stopped
17
+ a subject that had yet to start.
18
+ - A span's child whose cue a seek back plays again waits for the span's fit, as it did when first
19
+ cued, so the span lays it out as before. It started at once wherever its span's clock had passed
20
+ the start the fit first gave it: the fit then took it for ended, so it skipped nothing else, or,
21
+ in a span running back, the child ran back to its start and left.
22
+ - A child its owner's seek put back before its start stays where it was through a mix sought back to
23
+ after that seek: a later fit keeps it in its place, and an anchor does not place it again. The
24
+ seek took it for one still to start, so a span laid it out afresh, and a child anchored to a
25
+ sibling's end was placed again from that end, and played on as if its owner had not been sought.
26
+ - A voice cued while the mix is paused, its `start` a host time still ahead, waits for that time
27
+ again when a seek back plays its cue again. It started at once, at the paused mix time, since
28
+ the voice was taken for one cued while the mix slept and its start was moved to the next sync.
29
+ - A span whose budget runs out between two frames fits again from that moment, in turn with its
30
+ siblings' starts and ends: a child it places at a start already passed starts in that frame, and
31
+ one queued after it starts where it lapsed at any frame rate. The span lapsed only after the
32
+ frame had started its siblings, so a sibling whose old start the frame passed kept it, and one
33
+ placed in the past waited a frame to start.
34
+ - An owner running back holds a child placed where its clock stands unplayed, whether or not a
35
+ frame lands there. An owner cued backward with a child at 0 started that child, ended it, and
36
+ left with it only when a frame fell exactly on the owner's start; otherwise both waited for good.
37
+ - A read ahead that throws partway, on a call the tape holds that it cannot play, puts the tape
38
+ back. It left the live tape moved past the call, so a later seek restored the wrong state.
39
+ - Under `history`, a voice faded out of one subject is let go when history lets it go. The mix
40
+ held every such voice for good, and each `drop` walked them all.
41
+ - What `prepare` loads is let go once `history.ms` of mix time passes without a seek or read
42
+ taking it. Until one did, the mix kept every record paged since, for good.
43
+ - `trace` of a voice sought past its end, with a `fade.out`, follows the fade from the voice's
44
+ start, where the mix runs it. It called the run exact and read 0 throughout.
45
+ - `weightOf` with lanes on reports the weight a subject was given when its voice moved onto a lane
46
+ after the subject's probe that frame. It reported 0.
47
+ - A `pull` right after two or more cues folds the new voices in voice order, as a probe does. It
48
+ folded them newest first, which on a `mul` or `sum` channel could differ in the last bit.
49
+ - An owner's fade in counts from when the owner started, under an owner turned back or slowed.
50
+ It was worked out again each frame from where the outer owner's clock would next reach the
51
+ inner owner's start, so a clip under an inner owner with a fade in vanished when the outer
52
+ owner turned back, and slowing the outer owner jumped the fade to its end.
53
+ - A fade set for now or earlier, on a voice whose owner runs back, begins then. It was put off to
54
+ when the owner's clock would come back to the voice's start, playing at full weight until then.
55
+ - A fade in stands still while a fade out runs, and climbs on from there after `rise`, rather than
56
+ jumping to where its clock had reached. A subject's own fade out stops its fade in climbing too,
57
+ as the voice's does; before, the two multiplied into a rise.
58
+ - A span child with `faster` or `slower` at `Infinity` is retimed to fill its span. It got a rate
59
+ of Infinity, jumping to its end, or 0, and was skipped as never fitting.
60
+ - A `from: 'current'` voice whose stop lists fewer axes than its `vec` channel reads the missing
61
+ axes as the lerp does. They read NaN.
62
+ - `spring` and `glide` throw for an infinite parameter. A glide with an infinite `ms` or
63
+ `stiffness` read NaN, and a spring with infinite `damping` or `mass` jumped to its target.
64
+ - A role member at weight 0 holds its role's place whatever its patch's form, and a band switched
65
+ off by a frame at weight 0 stays off when the weight comes back between `off` and `on`. A `keys`
66
+ member at weight 0 was read and so held the role's place; any other was not, so its role folded
67
+ at the next member's place, which on a `last` channel changed which value won, and the role's
68
+ band, along with every band in a mix with a role in it, survived the frame at 0.
69
+ - A frame with span children in it costs less than before this release's span-order fixes, which
70
+ had made it about 5% dearer: a child's visit reads its owners' clocks once, and the voices
71
+ visited every frame, which include every voice an owner holds, are kept in a list of their own
72
+ rather than sifted through the due queue's heap and sorted each frame. The new `spans` bench row,
73
+ ten staggered clips cued under one owner every four frames, reads 5.0% faster than with the
74
+ fixes (0.141 to 0.134 ms a frame) and 3.4% faster than before them (0.179 to 0.173 on another
75
+ node), 7 rounds each, U 7.
76
+ - A voice an owner holds, first read while the owner runs back, shows at once instead of waiting
77
+ on its fade in. The fade in counted from where the owner's clock, running back, would next reach
78
+ the voice's start, a time still to come, so the voice read nothing until then.
79
+ - An owner sought forward passes the span children it crosses as played straight through,
80
+ whatever its rate: they start and end in order, and their span fits from each end. Sought ahead
81
+ while turned back, it started them in the order its clock running back put their starts, last
82
+ first, and ended none; turning forward later ended them all at once, out of order, and a span
83
+ holding spans left in a later frame at a time still to come, so a mix sought back left it
84
+ fading and its clips silent. A timeline's reel jumped ahead in reverse played its scenes slowed
85
+ and overlapping.
86
+ - A span child kept quiet at its start, its owner having run back to it, comes back and ends in
87
+ turn when a seek of the owner takes it past its end, and its span fits from that end. It stayed
88
+ quiet, so the children playing past it kept the rate they had before its end.
89
+ - Under `history`, an owner another owner holds no longer leaves once none of its children plays:
90
+ it stays with its own owner, quiet, so turning the outer owner back brings back a clip under an
91
+ owner inside it, as it does a direct child.
92
+ - A span an owner ran back over keeps the layout it made going forward. Before, each child it ran
93
+ back to the start of fitted the span again from there, and so did a seek back into it, counting
94
+ the children waiting for the clock to come back as playing: the ones still to start moved
95
+ earlier, and the playing ones changed speed.
96
+ - A span lays out its children the same at any frame rate. When a child ended between two frames,
97
+ its span fitted again from that moment, and a sibling whose start had passed by then, but which
98
+ no frame had started yet, was placed again from there at a new start and rate: two children of a
99
+ `stagger` span read 420 stepped every 10 ms and 389.47 every 25 ms.
100
+ - A voice under an owner whose rate ramps through 0 (from forward to back) ends where its passes
101
+ run out while the owner still runs forward. The time was taken as never, so it ended at the
102
+ frame that noticed, and a span holding it fitted again from there, its layout following the
103
+ frame rate.
104
+ - A placement whose names make a loop is taken where no mark in it waits on itself: a cue whose
105
+ `out` is its follow's `start`, the follow starting off the cue's `coast`, was refused as waiting
106
+ on itself, since the check followed every name whatever mark it asked for. A voice's `start`,
107
+ `in`, `coast` and the marks placed inside it wait only on what places its start; its `out` and
108
+ `end` on that and what places its out.
109
+ - A placement that waits on itself through a query by tag or by channel is refused at `cue`, as
110
+ one through names is: a voice starting after the one tagged `b`, cued beside one tagged `b`
111
+ starting after the one tagged `a`, was taken. A query is followed to the voices it can pick at
112
+ the cue, the voice being cued among them.
113
+ - A voice no owner holds, placed by a timestamp in its anchor, stays where it was placed through a
114
+ seek back. Each placing read the timestamp against the mix's offset at that moment, which a seek
115
+ moves, so after `seek(1000)` from 2100 an owner anchored to start at 2000 had started at 900, and
116
+ one anchored to end at 4000 left at 0. The timestamp is now read against the offset in force when
117
+ the voice was cued, as a plain `start` is.
118
+ - Under `history`, a span an owner holds no longer leaves once its budget is out and none of its
119
+ children plays: it stays with its owner, quiet, so turning the owner back brings back a clip
120
+ under a span, or under a span in a span, as it does one under an owner.
121
+ - A child cued into a span an owner ran back over queues after the children it holds. The span
122
+ was fitted again at the cue, counting the children waiting for the clock to come back as
123
+ playing, so the new one started beside them. No fit, from a cue, an arc, a sibling leaving or a
124
+ seek, moves a span run back over until the clock passes it again.
125
+ - A child whose rate a fit changed reads, run back over by an owner, what it read going forward. Its
126
+ clock read the new rate all the way back past where the fit changed it; it now runs back on the
127
+ clock it had before, and forward again the fit changes it at the same place.
128
+ - An owner sought past a span's child fits the rest of the span from where that child ended. The
129
+ owner's clock now put the end before the mix began, where a span read its own clock as 0, so the
130
+ children still to come were laid out from the span's start: a timeline's reel jumped into its
131
+ fifth sequence started the third scene beside the second.
132
+ - A span whose children end in one frame fits from each end in the order they came. They were
133
+ taken in cue order, so an end the frame noticed first could be one that a fit from an earlier
134
+ end would have moved, and two children of a `stagger` span read 509.5 stepped every 50 ms where
135
+ 1 ms steps read 525.3.
136
+ - A child a fit skipped reads, once its owner is sought or run back before the skip, what it read
137
+ going forward. One skipped while playing kept its jump to its end and read on from there, and
138
+ one skipped before it started, placed at its end at its start, played its passes backward from
139
+ its start.
140
+ - A span child its owner ran or was sought back over takes the change a fit made to it again as
141
+ the owner's clock comes forward to it, as the layout made going forward stands. The span was
142
+ fitted again there instead, with the children it had skipped counted as still playing or left
143
+ out, so the ones it had skipped together with them played on, slowed, beside them.
144
+ - An owner sought past a span lays the span out in that frame as played straight through. Its
145
+ children that started and ended in the frame were taken in cue order once their span had started,
146
+ each started and ended in one step, so a fit from one end missed the children it should have
147
+ moved.
148
+ - A span its owner was sought back before keeps the layout it made going forward, as one run
149
+ back over does. A child's end there fitted it again with the children ahead, left quiet by the
150
+ seek, counted as gone, so the playing ones slowed to fill the span.
151
+ - A mix sought back into a span plays on as it stood at that moment. A seek fitted every span
152
+ again from the moment sought, which a span played straight through is not, so its children's
153
+ rates followed where the seek landed. A span's end and `result` are now kept with its history and
154
+ put back by the seek.
155
+
156
+ ## 0.16.0
157
+
158
+ ### Added
159
+
160
+ - `ink()`, a color channel for colors that stand for a meaning (a status, a level, a team), beside
161
+ `color()` for colors as the real world shows them. It takes the same `oklab()` values. It travels
162
+ round the hue the short way, and overlapping voices blend round the hue too: the pose holds
163
+ `[Σw·L, Σw·a, Σw·b, Σw·C, Σw]` and shows the hue of `Σw·(a, b)` at the average chroma, so red and
164
+ blue at equal weight show a vivid purple rather than the grayed one `color()` shows. Near-opposite
165
+ hues swing round the wheel as their weights cross. `ink(last())` replaces, traveling round the
166
+ hue, and holds four numbers. `toHex` and `css` read a pose of either length. Both run on lanes.
167
+
168
+ ### Changed
169
+
170
+ - A `tween`, `spring` or `glide` on an `ink`, or on the deprecated `color({ lerp: 'oklch' })`,
171
+ goes round the hue rather than straight across each OKLab axis through gray. A tween reads as the
172
+ channel's lerp along its easing; a spring and a glide are solved in lightness, chroma and hue
173
+ with coverage alongside, so velocity carries through a retarget and the path bends rather than
174
+ starting over. What the patch takes and reports stays premultiplied OKLab, four numbers. A glide
175
+ with `bounds` on such a channel is refused at `cue`. Lanes read it as the general path does, to
176
+ the bit; such a voice on one subject no longer joins a crowd. `color()` is unchanged.
177
+
178
+ - Under `history`, turning an owner back (a rate below 0) brings back the children that already
179
+ left, so playback reverses completely. A child an owner holds no longer leaves when its passes
180
+ run out: it fades as before, then stays with its owner, `live` and showing nothing, until the
181
+ owner leaves, and comes back, climbing over its `fade.out`, once its clock is back in its
182
+ passes. Its `done` now resolves when its owner leaves rather than at its own end. The owner
183
+ leaves when it did before, and without `history` nothing changes.
184
+
185
+ ### Deprecated
186
+
187
+ - `color({ lerp })`: `lerp: 'oklch'` is `ink()`, which also blends overlapping voices round the
188
+ hue, and `lerp: 'oklab'` is `color()`. It keeps its old behavior for this release and warns once.
189
+ The playground loads compositions saved with it.
190
+
7
191
  ## 0.15.1
8
192
 
9
193
  ### Added
package/README.md CHANGED
@@ -151,7 +151,7 @@ do yet.
151
151
 
152
152
  ## Status
153
153
 
154
- v0.15.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
154
+ v0.16.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
155
155
  transitions, weasel its animator's tweens, sherpa its seams, and
156
156
  [klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
157
157
  part effects, and the environment a sign is lit by — on a mix each.
package/dist/ahead.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { ownBlends } from './blend.js';
2
2
  import { headOf } from './chain.js';
3
- import { open, sighted, weightAt } from './everyone.js';
4
3
  import { copyHeld, unread } from './history.js';
5
4
  import { markOf, voiceMarks } from './marks.js';
6
5
  import { Mixer } from './mixer.js';
@@ -21,13 +20,12 @@ import { unreached } from './unreached.js';
21
20
  export function playAhead(transport, c, copies, t, earlier) {
22
21
  const tape = transport.tape;
23
22
  const depth = tape?.undoDepth() ?? 0;
24
- let played = false;
25
23
  try {
26
24
  for (;;) {
27
25
  const e = nextEvent(tape, c, t);
28
26
  if (!(e < t))
29
27
  break;
30
- played = playTo(transport, c, copies, e) || played;
28
+ playTo(transport, c, copies, e);
31
29
  if (!carries(c))
32
30
  continue;
33
31
  // Placed now, as the next move would place them, while a target that has just left is here.
@@ -39,6 +37,7 @@ export function playAhead(transport, c, copies, t, earlier) {
39
37
  const on = new Mixer(copy.kit, { ...copy.opts, transport: next }, null);
40
38
  on.slot = copy.slot;
41
39
  on.projecting = true;
40
+ on.share = copy.share;
42
41
  copyAhead(copy, on);
43
42
  const stages = earlier.get(mix);
44
43
  if (stages === undefined)
@@ -49,11 +48,12 @@ export function playAhead(transport, c, copies, t, earlier) {
49
48
  }
50
49
  c = next;
51
50
  }
52
- played = playTo(transport, c, copies, t) || played;
51
+ playTo(transport, c, copies, t);
53
52
  }
54
53
  finally {
55
- if (played)
56
- tape?.goto(depth);
54
+ // A call that throws partway has moved the tape already, so this asks the tape, not `playTo`.
55
+ if (tape !== undefined && tape.undoDepth() !== depth)
56
+ tape.goto(depth);
57
57
  }
58
58
  return c;
59
59
  }
@@ -108,14 +108,13 @@ function nextEvent(tape, c, t) {
108
108
  }
109
109
  /**
110
110
  * Makes the calls the tape holds up to mix time `to` on a read ahead's copies and moves them
111
- * there. Answers whether it moved the tape.
111
+ * there.
112
112
  */
113
113
  function playTo(transport, c, copies, to) {
114
114
  const tape = transport.tape;
115
115
  const due = (at) => (c.pace === null ? at : c.pace.reading(at)) <= to;
116
116
  const next = tape?.timestampAt(tape.undoDepth());
117
117
  c.u = hostAhead(c, to);
118
- let played = false;
119
118
  if (next !== undefined && due(next)) {
120
119
  // A call may write what a copy shares with the voice it was copied from.
121
120
  for (const copy of c.members)
@@ -125,7 +124,6 @@ function playTo(transport, c, copies, to) {
125
124
  if (v.parted !== null)
126
125
  v.parted = new Map(v.parted);
127
126
  }
128
- played = true;
129
127
  const last = replay(transport, due, c, {
130
128
  transport: () => c,
131
129
  mix: (of) => copies.get(of),
@@ -136,7 +134,6 @@ function playTo(transport, c, copies, to) {
136
134
  if (!(c.now >= to))
137
135
  for (const copy of c.members)
138
136
  move(copy, to);
139
- return played;
140
137
  }
141
138
  /** A transport for a read ahead's copies to go on from where those on `c` stand. */
142
139
  function restage(c) {
@@ -196,11 +193,12 @@ export function recordOf(mix, voice, subject) {
196
193
  // the subject for the caller to copy from.
197
194
  if (voice.sharing) {
198
195
  const slot = headOf(mix, subject)?.slot ?? -1;
199
- if (!sighted(voice, slot))
196
+ const share = mix.share;
197
+ if (!share.sighted(voice, slot))
200
198
  return undefined;
201
199
  const all = voice.everyone;
202
- open(voice, all, slot);
203
- all.weight = weightAt(voice, slot);
200
+ share.open(voice, all, slot);
201
+ all.weight = share.weightAt(voice, slot);
204
202
  return all;
205
203
  }
206
204
  const held = voice.subjects.get(subject);
package/dist/chain.js CHANGED
@@ -1,4 +1,3 @@
1
- import { everyoneOf, sight } from './everyone.js';
2
1
  import { record } from './record.js';
3
2
  /** The first record of a subject no live voice reaches, so a probe of it still makes one lookup. */
4
3
  function stub() {
@@ -98,10 +97,10 @@ export function linkable(voice, subject, slot, guess = null) {
98
97
  if (state === 'done' || (state === 'pending' && !voice.freezesBefore))
99
98
  return null;
100
99
  // A voice sharing one record is in no chain: a fold takes it from `sharers` at its place.
101
- if (voice.sharing && everyoneOf(this, voice) !== null) {
100
+ if (voice.sharing && this.share.everyoneOf(this, voice) !== null) {
102
101
  // A projection numbers no subject, and keeps nothing by subject.
103
102
  if (slot >= 0)
104
- sight(voice, slot, this.lanes.cap);
103
+ this.share.sight(voice, slot, this.lanes.cap);
105
104
  return null;
106
105
  }
107
106
  const held = this.held(voice, subject, slot, guess);
package/dist/clock.d.ts CHANGED
@@ -9,6 +9,16 @@ export declare function passesOf(loop: boolean | number | undefined): number;
9
9
  export declare function phaseAt(elapsed: number, duration: number, passes: number): number;
10
10
  /** The pass `elapsed` voice ms falls in, held at the last once they are done. */
11
11
  export declare function passAt(elapsed: number, duration: number, passes: number): number;
12
+ /**
13
+ * A fade in from `since` over `fadeIn` at mix time `now`, 0..1. It stands still where a fade out
14
+ * began, or the two ramps multiply into a rise, from `stop` on, and through `stalls`, pairs of mix
15
+ * times.
16
+ */
17
+ export declare function fadeInAt(fadeIn: number, out: {
18
+ at: number;
19
+ over: number;
20
+ rest: boolean;
21
+ } | null, ease: Curve | undefined, now: number, since: number, stalls: readonly number[] | null, stop?: number): number;
12
22
  /**
13
23
  * A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, out along its ramp, and back
14
24
  * up the same curve where a fade out was turned around.
@@ -21,7 +31,7 @@ export declare function envelope(fadeIn: number, out: {
21
31
  from: number;
22
32
  at: number;
23
33
  over: number;
24
- } | null): number;
34
+ } | null, stalls?: readonly number[] | null): number;
25
35
  /**
26
36
  * A subject's voice time `raw` once the voice's freezes apply: 0 before it starts if it freezes
27
37
  * before, else NaN, where it shows nothing; `span` once its passes end if it freezes after. A `raw`
package/dist/clock.js CHANGED
@@ -21,19 +21,36 @@ export function passAt(elapsed, duration, passes) {
21
21
  return 0;
22
22
  return over(elapsed, duration, passes) ? passes - 1 : Math.floor(elapsed / duration);
23
23
  }
24
+ /**
25
+ * A fade in from `since` over `fadeIn` at mix time `now`, 0..1. It stands still where a fade out
26
+ * began, or the two ramps multiply into a rise, from `stop` on, and through `stalls`, pairs of mix
27
+ * times.
28
+ */
29
+ export function fadeInAt(fadeIn, out, ease, now, since, stalls, stop = Number.POSITIVE_INFINITY) {
30
+ let end = out && !out.rest && out.at < now ? out.at : now;
31
+ if (stop < end)
32
+ end = stop;
33
+ let climbed = end - since;
34
+ if (stalls !== null)
35
+ for (let i = 0; i < stalls.length; i += 2) {
36
+ const a = Math.max(stalls[i], since);
37
+ const b = Math.min(stalls[i + 1], end);
38
+ if (b > a)
39
+ climbed -= b - a;
40
+ }
41
+ const u = climbed / fadeIn;
42
+ if (u >= 1)
43
+ return 1;
44
+ return ease ? ease(Math.max(0, u)) : Math.max(0, u);
45
+ }
24
46
  /**
25
47
  * A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, out along its ramp, and back
26
48
  * up the same curve where a fade out was turned around.
27
49
  */
28
- export function envelope(fadeIn, out, ease, reduced, now, since, back = null) {
50
+ export function envelope(fadeIn, out, ease, reduced, now, since, back = null, stalls = null) {
29
51
  let w = 1;
30
- if (fadeIn > 0 && !reduced) {
31
- // A fade in stops climbing where a fade out begins, or the two ramps multiply into a rise.
32
- const end = out && !out.rest && out.at < now ? out.at : now;
33
- const u = (end - since) / fadeIn;
34
- if (u < 1)
35
- w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
36
- }
52
+ if (fadeIn > 0 && !reduced)
53
+ w *= fadeInAt(fadeIn, out, ease, now, since, stalls);
37
54
  if (back && !reduced) {
38
55
  const u = back.from + (now - back.at) / back.over;
39
56
  if (u < 1)
@@ -114,7 +131,9 @@ export function timeWith(c, elapsed) {
114
131
  return r.from > 0 ? c.anchorNow + (elapsed - e0) / r.from : c.anchorNow;
115
132
  const a = (r.to - r.from) / (2 * r.over);
116
133
  const atEnd = e0 + r.from * r.over + a * r.over * r.over;
117
- if (elapsed <= atEnd) {
134
+ // A ramp that turns the clock back reads furthest where its rate crosses 0, not at its end.
135
+ const top = r.from > 0 && r.to < 0 ? e0 - (r.from * r.from) / (4 * a) : atEnd;
136
+ if (elapsed <= top) {
118
137
  const k = e0 - elapsed;
119
138
  const dt = Math.abs(a) < 1e-12
120
139
  ? -k / r.from
package/dist/color.d.ts CHANGED
@@ -8,44 +8,66 @@ import type { Channel } from './types.js';
8
8
  */
9
9
  export declare function mixHex(a: number, b: number, u: number): number;
10
10
  /**
11
- * How a `color` channel interpolates two colors. Straight across OKLab, or in OKLCH with hue the
12
- * short way round, as `mixHex` does.
11
+ * What `color` took before `ink` existed: `lerp: 'oklch'` overlaps straight across OKLab and
12
+ * travels round the hue.
13
13
  *
14
14
  * @category channel
15
15
  */
16
16
  export interface ColorOptions {
17
+ /** @deprecated `'oklch'` is `ink()`, which also blends overlapping voices round the hue; `'oklab'` is `color()`. */
17
18
  lerp?: 'oklab' | 'oklch';
18
19
  }
19
20
  /**
20
- * Color as OKLab `[L, a, b, coverage]`, its merge rule given as `of`. `color()` averages: voices
21
- * stack as a premultiplied sum, so a pose holds `[Σw·L, Σw·a, Σw·b, Σw]` and runs on lanes;
22
- * `toHex` divides it out and lays it over what is under it. `color(last())` replaces, the last
23
- * voice to pass winning, at the coverage it was authored with. `lerp` picks how two colors
24
- * interpolate: OKLab, the default, crosses through gray between opposite hues, and `'oklch'`
25
- * goes round the hue instead.
21
+ * Whether `channel` travels round the hue, as `ink` does, so a motion patch cued on it moves round
22
+ * the hue too.
23
+ */
24
+ export declare function roundsHue(channel: Channel<unknown>): boolean;
25
+ /**
26
+ * Color as the real world shows it, light, sky, smoke or paint: OKLab `[L, a, b, coverage]`, its
27
+ * merge rule given as `of`. `color()` averages: voices stack as a premultiplied sum, so a pose
28
+ * holds `[Σw·L, Σw·a, Σw·b, Σw]` and runs on lanes; `toHex` divides it out and lays it over what
29
+ * is under it. `color(last())` replaces, the last voice to pass winning, at the coverage it was
30
+ * authored with. It travels between two colors, in a motion or a role's handoff, straight across
31
+ * OKLab, so opposite hues cross through gray. For colors that stand for a meaning, use `ink`.
26
32
  *
27
33
  * @category channel
28
34
  */
29
35
  export declare function color(opts?: ColorOptions): Channel<number[]>;
30
36
  export declare function color(of: Channel<number> | Channel<unknown>, opts?: ColorOptions): Channel<number[]>;
31
37
  /**
32
- * Converts 0xrrggbb to a `color` value. The value is OKLab `[L, a, b]` at full coverage. Convert
38
+ * Color standing for a meaning, a status, a level or a team, rather than for a physical
39
+ * phenomenon: choose it over `color` when the colors are symbols. People picture the midpoint
40
+ * between red and blue as purple, reached by turning the hue, not as the grayed mix light makes,
41
+ * and `ink` gives them that. It travels round the hue the short way, in a motion, between keys
42
+ * and in a role's handoff, and `ink()` blends overlapping voices round the hue too: each voice
43
+ * sums as `[w·L, w·a, w·b, w·C, w]`, C its chroma, so a pose shows lightness `ΣwL/Σw`, the hue of
44
+ * `Σw·(a, b)`, which a gray voice pulls on not at all, and chroma `ΣwC/Σw`, so two vivid colors
45
+ * stay vivid. Near-opposite hues overlapping swing round the wheel as their weights cross, with
46
+ * no tiebreak, and exactly opposite ones cancel to gray. A patch writes it the `oklab()` values a
47
+ * `color` takes, and `toHex` and `css` read either. `ink(last())` replaces, traveling round the
48
+ * hue, and holds the four numbers it was written, since it never sums.
49
+ *
50
+ * @category channel
51
+ */
52
+ export declare function ink(of?: Channel<number> | Channel<unknown>): Channel<number[]>;
53
+ /**
54
+ * Converts 0xrrggbb to a `color` or `ink` value. The value is OKLab `[L, a, b]` at full coverage. Convert
33
55
  * keyframe stops once when authoring them, and call it outside the hot path in an `fn` patch.
34
56
  *
35
57
  * @category channel
36
58
  */
37
59
  export declare function oklab(rgb: number): number[];
38
60
  /**
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`.
61
+ * A `color` or `ink` pose as 0xrrggbb, laid over `under`. Its color is divided out of its
62
+ * coverage, the coverage capped at 1 and laid over `under` in OKLab, then clipped to sRGB per
63
+ * channel. No coverage gives `under`.
42
64
  *
43
65
  * @category channel
44
66
  */
45
67
  export declare function toHex(pose: ArrayLike<number>, under?: number): number;
46
68
  /**
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.
69
+ * A `color` or `ink` pose as a CSS `oklab()` color, its coverage as the alpha. The coverage is
70
+ * capped at 1. For a host that lets the browser lay it over what is under it.
49
71
  *
50
72
  * @category channel
51
73
  */