@msb235/blits 0.8.1 → 0.9.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +12 -2
  3. package/dist/clock.d.ts +10 -0
  4. package/dist/clock.js +23 -0
  5. package/dist/cue.js +5 -8
  6. package/dist/due.js +20 -13
  7. package/dist/everyone.js +3 -0
  8. package/dist/fade.d.ts +1 -0
  9. package/dist/fade.js +10 -4
  10. package/dist/fill.js +5 -0
  11. package/dist/fold.js +28 -22
  12. package/dist/gone.d.ts +4 -4
  13. package/dist/gone.js +11 -10
  14. package/dist/handle.js +17 -9
  15. package/dist/held.d.ts +10 -0
  16. package/dist/held.js +28 -1
  17. package/dist/hosts.js +5 -1
  18. package/dist/keyfill.js +5 -1
  19. package/dist/locus.d.ts +4 -1
  20. package/dist/locus.js +12 -2
  21. package/dist/marks.js +4 -4
  22. package/dist/motion.d.ts +6 -3
  23. package/dist/motions.d.ts +5 -2
  24. package/dist/motions.js +3 -1
  25. package/dist/move.js +51 -14
  26. package/dist/origin.js +3 -2
  27. package/dist/owner.d.ts +2 -0
  28. package/dist/owner.js +10 -2
  29. package/dist/place.js +17 -3
  30. package/dist/project.d.ts +1 -1
  31. package/dist/project.js +11 -6
  32. package/dist/record.js +2 -1
  33. package/dist/revive.d.ts +2 -0
  34. package/dist/revive.js +2 -0
  35. package/dist/seek.js +2 -2
  36. package/dist/spans.d.ts +5 -4
  37. package/dist/spans.js +11 -10
  38. package/dist/strays.js +13 -5
  39. package/dist/transport.js +4 -2
  40. package/dist/types/mix.d.ts +3 -2
  41. package/dist/types/patch.d.ts +7 -3
  42. package/dist/types/place.d.ts +4 -1
  43. package/dist/types/voice.d.ts +22 -4
  44. package/dist/unpage.d.ts +1 -1
  45. package/dist/unpage.js +2 -3
  46. package/dist/voice.d.ts +52 -0
  47. package/dist/voice.js +103 -2
  48. package/dist/warn.d.ts +2 -0
  49. package/dist/warn.js +17 -0
  50. package/dist/weigh.js +5 -0
  51. package/docs/schema.html +92 -29
  52. package/package.json +2 -2
@@ -39,12 +39,16 @@ export interface Setting<S = void, H = unknown> {
39
39
  * The gap this subject is catching up by: milliseconds since it last advanced, which is the gap
40
40
  * since the previous sync only for a subject sampled every frame. Infinity under reduced motion;
41
41
  * no more than `maxDt` where the mix sets one. Inside a `step` under `stepMs`, it is `stepMs`, and
42
- * `timestamp` is the end of that interval.
42
+ * `timestamp` is the end of that interval. A lane samples a subject for one frame after its last
43
+ * probe. A read by `project` is one sample, so its gap is not the gap a frame of play has.
43
44
  */
44
45
  dt: number;
45
- /** Milliseconds this voice has been playing, rate applied. */
46
+ /**
47
+ * Milliseconds this voice has been playing, rate applied. Below 0 only for a voice with no end
48
+ * cued backward, which plays into the passes before its first.
49
+ */
46
50
  elapsed: number;
47
- /** Which pass this is, 0 first. */
51
+ /** Which pass this is, 0 first; below 0 where `elapsed` is. */
48
52
  pass: number;
49
53
  /** This voice's weight for this subject this frame, after fades, signals and any locus. */
50
54
  weight: number;
@@ -55,7 +55,10 @@ export type Anchor = {
55
55
  * Where a voice sits on the mix clock: at most one of `start` and `in`, and at most one of `out`
56
56
  * and `end`, each a timestamp on the host's clock or an anchor to another voice. A voice whose
57
57
  * anchor has no answer yet waits pending; one whose anchored mark is already past starts partway
58
- * through, as playback does from the middle of a region.
58
+ * through, as playback does from the middle of a region. A fade out begins no earlier than the
59
+ * voice starts: an `end` that leaves less room than `fade.out` shortens the fade to fit, and one
60
+ * before the start makes the voice an instant, which starts and ends at its start and shows
61
+ * nothing.
59
62
  *
60
63
  * @category score
61
64
  */
@@ -61,15 +61,30 @@ export interface VoiceSpec<I, O, H = unknown> extends SpanHints {
61
61
  subjects?: readonly I[];
62
62
  /**
63
63
  * When the voice starts, in ms on the host's clock, the one it passes `sync`: a rAF timestamp and
64
- * `performance.now()` share an origin, `Date.now()` does not. Default: the next sync.
64
+ * `performance.now()` share an origin, `Date.now()` does not. Default: the time the mix last
65
+ * synced to, and 0 for a voice cued before the first sync, which that sync finds already as far
66
+ * in as its timestamp.
65
67
  */
66
68
  start?: number;
67
- /** Playback rate. 1 is real time; 0 freezes. */
69
+ /**
70
+ * Playback rate. 1 is real time; 0 freezes; below 0 plays backward, to end where the voice
71
+ * starts as a voice playing forward ends where its last pass does: `played` resolves, and it
72
+ * leaves over `fade.out` or, freezing before, holds its first frame.
73
+ */
68
74
  rate?: number;
75
+ /**
76
+ * Where in itself the voice starts, in voice ms, as `seek` on its handle sets. Default 0, or
77
+ * for a voice cued backward its end: `duration` times its passes. A voice with no end cued
78
+ * backward (a loop for good, or a patch with no duration) starts at 0 and has no first pass
79
+ * either: it plays into the pass before, `setting.pass` and `setting.elapsed` below 0, for
80
+ * every subject from the voice's start whatever its stagger. A motion's stretches begin at 0,
81
+ * so one cued backward with no `seek` has ended as it starts.
82
+ */
83
+ seek?: number;
69
84
  /**
70
85
  * true loops for good, false plays one pass, n plays n passes. A finite loop leaves when its
71
86
  * passes are done for the latest-staggered subject it has seen — over `fade.out` where one is
72
- * set, at once where none is — unless it freezes after.
87
+ * set, at once where none is — unless it freezes after. Default: true.
73
88
  */
74
89
  loop?: boolean | number;
75
90
  /**
@@ -249,7 +264,8 @@ export interface Handle<I = unknown> {
249
264
  weight: number;
250
265
  /**
251
266
  * Playback rate now. Setting it changes speed at once; `ramp` eases into a new one. On a voice
252
- * still pending, a rate above 0 applies from the voice's start, which stays where it is.
267
+ * still pending, a rate or a ramp applies from the voice's start, which stays where it is: at
268
+ * rate 0 it starts on time and holds its first frame.
253
269
  */
254
270
  rate: number;
255
271
  /**
@@ -265,6 +281,8 @@ export interface Handle<I = unknown> {
265
281
  * it was. Answers how sure the state now is: `exact` for a voice with none, or rebuilt under
266
282
  * `stepMs` with no `maxDt`; `stepped` where it was caught up in one step or holds signal state;
267
283
  * `held` where it was kept, or depends on host fields or an input weight the rebuild cannot know.
284
+ * A voice still pending starts on time, already at `elapsed`, and one that freezes before shows
285
+ * that frame while it waits.
268
286
  */
269
287
  seek(elapsed: number, opts?: SeekOptions): Doubt;
270
288
  /**
package/dist/unpage.d.ts CHANGED
@@ -5,4 +5,4 @@ import type { Transport } from './transport.js';
5
5
  */
6
6
  export declare function cover(transport: Transport, t: number): boolean;
7
7
  /** A seek back to frame `seq`: what the store and `prepare` hold after it is from a future the tape makes again. */
8
- export declare function cut(transport: Transport, seq: number, unpaged: boolean): void;
8
+ export declare function cut(transport: Transport, seq: number): void;
package/dist/unpage.js CHANGED
@@ -30,12 +30,11 @@ export function cover(transport, t) {
30
30
  return true;
31
31
  }
32
32
  /** A seek back to frame `seq`: what the store and `prepare` hold after it is from a future the tape makes again. */
33
- export function cut(transport, seq, unpaged) {
33
+ export function cut(transport, seq) {
34
34
  const pager = transport.pager;
35
35
  if (pager === null)
36
36
  return;
37
- if (unpaged)
38
- pager.store.cut(seq);
37
+ pager.store.cut(seq);
39
38
  const loaded = pager.loaded;
40
39
  if (loaded !== null)
41
40
  loaded.records = loaded.records.filter((r) => r.seq <= seq);
package/dist/voice.d.ts CHANGED
@@ -127,6 +127,8 @@ export interface Ramp {
127
127
  over: number;
128
128
  rest: boolean;
129
129
  deadline?: number;
130
+ /** Begun where the voice ran back to its start, whose first frame it holds through the fade. */
131
+ back?: boolean;
130
132
  }
131
133
  /**
132
134
  * A climb back from a fade out that was turned around: from `from`, the place along the fade's
@@ -224,6 +226,12 @@ export declare class Voice<I, O> {
224
226
  readonly span: number;
225
227
  /** The state behind a motion patch, undefined for any other. */
226
228
  readonly motion: Motions<I> | undefined;
229
+ /**
230
+ * Whether it has no first pass: cued backward with no end to start from, it plays from 0 into
231
+ * the pass before, so no time on its clock is before its start. Never a motion, whose stretches
232
+ * begin at 0.
233
+ */
234
+ readonly beginless: boolean;
227
235
  /** Its `freeze`, for a patch it applies to: motion keeps its target already. */
228
236
  readonly freezesBefore: boolean;
229
237
  readonly freezesAfter: boolean;
@@ -244,6 +252,8 @@ export declare class Voice<I, O> {
244
252
  seen: number;
245
253
  /** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
246
254
  latest: number;
255
+ /** The least stagger of any subject seen, 0 or below, which a voice running back ends at. */
256
+ soonest: number;
247
257
  restedCount: number;
248
258
  /** The mix time it was cued at; -Infinity before the first sync. */
249
259
  cuedAt: number;
@@ -253,6 +263,10 @@ export declare class Voice<I, O> {
253
263
  doneAt: number;
254
264
  /** The frame its leaving was decided in, which a seek or read back partitions by. */
255
265
  doneSeq: number;
266
+ /** The mix time of that frame, later than `doneAt` for a leaving put behind now. */
267
+ decidedAt: number;
268
+ /** The latest mix time a seek or read back to finds it still playing. */
269
+ get reachedTo(): number;
256
270
  /** Under `history`, its controls after each change, oldest first. */
257
271
  log: Controls[] | null;
258
272
  /** Under `history`, its controls as `cue` left them, which a voice parked by a seek takes back. */
@@ -361,6 +375,44 @@ export declare class Voice<I, O> {
361
375
  elapsedAt(now: number): number;
362
376
  /** The mix time its clock reads `elapsed`, inverting `elapsedAt`; Infinity where it never will. */
363
377
  timeAt(elapsed: number): number;
378
+ /**
379
+ * Whether its clock runs back by its controls: at a rate below 0, or on a ramp between rates
380
+ * neither of which is above it. A ramp through 0 says neither way until it has turned.
381
+ */
382
+ get backs(): boolean;
383
+ /** Whether its ramp takes its rate through 0, which turns its clock around partway. */
384
+ get turns(): boolean;
385
+ /**
386
+ * The mix time its passes run out by its controls now: its last pass's end running forward, its
387
+ * first pass's start running back. Infinity where they never do, and while a ramp has yet to
388
+ * turn it around.
389
+ */
390
+ endsAt(): number;
391
+ /**
392
+ * The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
393
+ * `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.
394
+ */
395
+ ranBackTo(elapsed: number, now: number): number;
396
+ /** Whether it has yet to start at mix time `now`: pending, or fading out from before its start. */
397
+ unstarted(now: number): boolean;
398
+ /**
399
+ * A subject's voice time at mix time `now`, `delay` into the voice, before its freezes apply. A
400
+ * voice yet to start reads where it will: its clock is anchored there, and read at any time
401
+ * before would have a voice sought ahead, or running back, play before its start.
402
+ */
403
+ rawAt(now: number, delay: number): number;
404
+ /**
405
+ * Whether a subject at `raw` is before its start at mix time `now`: it shows nothing, or its
406
+ * first frame frozen.
407
+ */
408
+ before(raw: number, now: number): boolean;
409
+ /**
410
+ * Whether a subject before its start shows its first frame: the voice freezes before, or is
411
+ * fading out from where it ran back to its start, as one fading from its end shows its last.
412
+ */
413
+ get holdsFirst(): boolean;
414
+ /** A subject's voice time at `raw` once the voice's freezes apply; NaN where it shows nothing. */
415
+ shownAt(raw: number): number;
364
416
  /** Records this voice's controls as they stand, from mix time `at` in frame `seq`. */
365
417
  note(at: number, seq: number, sync?: boolean): void;
366
418
  /**
package/dist/voice.js CHANGED
@@ -1,8 +1,8 @@
1
- import { elapsedWith, passesOf, rateWith, rebaseWith, timeWith } from './clock.js';
1
+ import { backWith, elapsedWith, frozenTime, passesOf, rateWith, rebaseWith, timeWith, } from './clock.js';
2
2
  import { curve } from './easing.js';
3
3
  import { motionOf } from './motion.js';
4
4
  import { Named } from './named.js';
5
- import { childPlayed, Holding, mixTime, ownedElapsed, ownerPatch } from './owner.js';
5
+ import { childPlayed, Holding, mixTime, ownedElapsed, ownerPatch, ownerReading, turnedBack, } from './owner.js';
6
6
  import { builtOf, intosOf } from './patch.js';
7
7
  import { reading } from './reading.js';
8
8
  import { Store } from './store.js';
@@ -154,6 +154,12 @@ export class Voice {
154
154
  span;
155
155
  /** The state behind a motion patch, undefined for any other. */
156
156
  motion;
157
+ /**
158
+ * Whether it has no first pass: cued backward with no end to start from, it plays from 0 into
159
+ * the pass before, so no time on its clock is before its start. Never a motion, whose stretches
160
+ * begin at 0.
161
+ */
162
+ beginless;
157
163
  /** Its `freeze`, for a patch it applies to: motion keeps its target already. */
158
164
  freezesBefore;
159
165
  freezesAfter;
@@ -174,6 +180,8 @@ export class Voice {
174
180
  seen = 0;
175
181
  /** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
176
182
  latest = 0;
183
+ /** The least stagger of any subject seen, 0 or below, which a voice running back ends at. */
184
+ soonest = 0;
177
185
  restedCount = 0;
178
186
  /** The mix time it was cued at; -Infinity before the first sync. */
179
187
  cuedAt = Number.NEGATIVE_INFINITY;
@@ -183,6 +191,12 @@ export class Voice {
183
191
  doneAt = Number.POSITIVE_INFINITY;
184
192
  /** The frame its leaving was decided in, which a seek or read back partitions by. */
185
193
  doneSeq = Number.POSITIVE_INFINITY;
194
+ /** The mix time of that frame, later than `doneAt` for a leaving put behind now. */
195
+ decidedAt = Number.NEGATIVE_INFINITY;
196
+ /** The latest mix time a seek or read back to finds it still playing. */
197
+ get reachedTo() {
198
+ return this.doneAt > this.decidedAt ? this.doneAt : this.decidedAt;
199
+ }
186
200
  /** Under `history`, its controls after each change, oldest first. */
187
201
  log = null;
188
202
  /** Under `history`, its controls as `cue` left them, which a voice parked by a seek takes back. */
@@ -363,6 +377,7 @@ export class Voice {
363
377
  this.early = null;
364
378
  this.seen = 0;
365
379
  this.latest = 0;
380
+ this.soonest = 0;
366
381
  this.restedCount = 0;
367
382
  this.cuedAt = Number.NEGATIVE_INFINITY;
368
383
  this.cuedSeq = Number.NEGATIVE_INFINITY;
@@ -419,7 +434,11 @@ export class Voice {
419
434
  this.setting = new VoiceSetting(host, send, this);
420
435
  this.rate = spec.rate ?? 1;
421
436
  this.weight = typeof spec.weight === 'number' ? spec.weight : 1;
437
+ const endless = !Number.isFinite(this.span);
438
+ this.beginless = this.rate < 0 && endless && this.motion === undefined;
422
439
  this.anchorNow = start;
440
+ // Where it starts: a voice cued backward at its end, where it has one.
441
+ this.anchorElapsed = spec.seek ?? (this.rate < 0 && !endless ? this.span : 0);
423
442
  if (now >= start)
424
443
  this.state = 'live';
425
444
  }
@@ -432,6 +451,88 @@ export class Voice {
432
451
  const t = timeWith(this, elapsed);
433
452
  return this.owner === null ? t : mixTime(this.owner, t);
434
453
  }
454
+ /**
455
+ * Whether its clock runs back by its controls: at a rate below 0, or on a ramp between rates
456
+ * neither of which is above it. A ramp through 0 says neither way until it has turned.
457
+ */
458
+ get backs() {
459
+ const r = this.ramp;
460
+ return r === null ? this.rate < 0 : r.from <= 0 && r.to <= 0 && (r.from < 0 || r.to < 0);
461
+ }
462
+ /** Whether its ramp takes its rate through 0, which turns its clock around partway. */
463
+ get turns() {
464
+ const r = this.ramp;
465
+ return r !== null && r.from * r.to < 0;
466
+ }
467
+ /**
468
+ * The mix time its passes run out by its controls now: its last pass's end running forward, its
469
+ * first pass's start running back. Infinity where they never do, and while a ramp has yet to
470
+ * turn it around.
471
+ */
472
+ endsAt() {
473
+ if (this.turns)
474
+ return Number.POSITIVE_INFINITY;
475
+ const backs = this.backs;
476
+ let to;
477
+ if (backs !== (this.owner !== null && turnedBack(this))) {
478
+ if (this.beginless)
479
+ return Number.POSITIVE_INFINITY;
480
+ to = this.soonest;
481
+ }
482
+ else {
483
+ if (!Number.isFinite(this.span))
484
+ return Number.POSITIVE_INFINITY;
485
+ to = this.span + this.latest;
486
+ }
487
+ const t = backs ? backWith(this, to) : timeWith(this, to);
488
+ return this.owner === null ? t : mixTime(this.owner, t);
489
+ }
490
+ /**
491
+ * The mix time its clock ran back to `elapsed` by `now`, by its controls now: where a subject
492
+ * `elapsed` into it reached its start. NaN where it has not, and for a voice an owner holds.
493
+ */
494
+ ranBackTo(elapsed, now) {
495
+ if (this.beginless || !this.backs || this.owner !== null)
496
+ return Number.NaN;
497
+ const t = backWith(this, elapsed);
498
+ return t >= this.anchorNow && t <= now ? t : Number.NaN;
499
+ }
500
+ /** Whether it has yet to start at mix time `now`: pending, or fading out from before its start. */
501
+ unstarted(now) {
502
+ const state = this.state;
503
+ if (state === 'pending')
504
+ return true;
505
+ if (state !== 'fading')
506
+ return false;
507
+ return !((this.owner === null ? now : ownerReading(this.owner, now)) >= this.start);
508
+ }
509
+ /**
510
+ * A subject's voice time at mix time `now`, `delay` into the voice, before its freezes apply. A
511
+ * voice yet to start reads where it will: its clock is anchored there, and read at any time
512
+ * before would have a voice sought ahead, or running back, play before its start.
513
+ */
514
+ rawAt(now, delay) {
515
+ return (this.unstarted(now) ? this.anchorElapsed : this.elapsedAt(now)) - delay;
516
+ }
517
+ /**
518
+ * Whether a subject at `raw` is before its start at mix time `now`: it shows nothing, or its
519
+ * first frame frozen.
520
+ */
521
+ before(raw, now) {
522
+ // Not `raw < 0`: see `frozenTime` for the clock that reads NaN before its start.
523
+ return (!(raw >= 0) && !this.beginless) || this.unstarted(now);
524
+ }
525
+ /**
526
+ * Whether a subject before its start shows its first frame: the voice freezes before, or is
527
+ * fading out from where it ran back to its start, as one fading from its end shows its last.
528
+ */
529
+ get holdsFirst() {
530
+ return this.freezesBefore || this.out?.back === true;
531
+ }
532
+ /** A subject's voice time at `raw` once the voice's freezes apply; NaN where it shows nothing. */
533
+ shownAt(raw) {
534
+ return this.beginless ? raw : frozenTime(raw, this.holdsFirst, this.freezesAfter, this.span);
535
+ }
435
536
  /** Records this voice's controls as they stand, from mix time `at` in frame `seq`. */
436
537
  note(at, seq, sync = false) {
437
538
  this.log?.push({
package/dist/warn.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Warns outside production, once for each `about`. */
2
+ export declare function warnOnce(about: object, message: string): void;
package/dist/warn.js ADDED
@@ -0,0 +1,17 @@
1
+ /** Whether this is a production build: a bundler's `NODE_ENV`, or Node's; false where neither is. */
2
+ function production() {
3
+ try {
4
+ return process.env.NODE_ENV === 'production';
5
+ }
6
+ catch {
7
+ return false;
8
+ }
9
+ }
10
+ const warned = new WeakSet();
11
+ /** Warns outside production, once for each `about`. */
12
+ export function warnOnce(about, message) {
13
+ if (warned.has(about) || production())
14
+ return;
15
+ warned.add(about);
16
+ console.warn(`blits: ${message}`);
17
+ }
package/dist/weigh.js CHANGED
@@ -6,6 +6,9 @@ import { reading } from './reading.js';
6
6
  export function prime(voice, held, now, elapsed, pass) {
7
7
  const setting = voice.setting;
8
8
  setting.timestamp = now;
9
+ // With no step to move it, `stepped` follows the call before this frame's.
10
+ if (voice.patch.step === undefined && held.probed < now)
11
+ held.stepped = held.probed;
9
12
  setting.dt = this.capped(now - held.stepped);
10
13
  setting.elapsed = elapsed;
11
14
  setting.pass = pass;
@@ -43,6 +46,8 @@ export function ownerBase(owner, subject) {
43
46
  if (held.probed === this.frame)
44
47
  return held.phase;
45
48
  const kept = reading.kept;
49
+ // An owner's record keeps a frame in `probed`, not a time, so `prime` leaves its `stepped` be.
50
+ held.probed = Number.NaN;
46
51
  this.prime(owner, held, now, owner.elapsedAt(now), 0);
47
52
  const base = this.base(owner, subject, now, held);
48
53
  reading.kept = kept;