@msb235/blits 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mixer.d.ts CHANGED
@@ -114,6 +114,13 @@ export declare class Mixer<I, O> implements Mix<I, O> {
114
114
  reducedNow: boolean;
115
115
  /** Something was changed since the last sync, so the next frame may differ from this one. */
116
116
  stirred: boolean;
117
+ /**
118
+ * The host read `inert` as true since the last sync, so its frame loop may be asleep until a
119
+ * change wakes it; a host that never asks keeps no loop asleep on it.
120
+ */
121
+ asleep: boolean;
122
+ /** Some voice cued while the mix slept waits for the next sync to set its start. */
123
+ waking: boolean;
117
124
  /** Counts the changes `stir` records, which a frame's record of a probe is current only before. */
118
125
  stirs: number;
119
126
  syncing: boolean;
@@ -259,6 +266,7 @@ export declare class Mixer<I, O> implements Mix<I, O> {
259
266
  voices(tag?: string): Handle<I>[];
260
267
  get live(): boolean;
261
268
  get inert(): boolean;
269
+ private restful;
262
270
  get rate(): number;
263
271
  set rate(r: number);
264
272
  ramp(rate: number, over: number): void;
package/dist/mixer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { cueArc } from './arcs.js';
2
2
  import { blended, blendOf, laneRead, shareKept } from './blend.js';
3
3
  import { aims, chain, headOf, linkable, relink, unlink } from './chain.js';
4
- import { lerpInto, scalarOp } from './channels.js';
4
+ import { lerpInto, STOCK_BAND, scalarOp } from './channels.js';
5
5
  import { cue } from './cue.js';
6
6
  import { DepartedIndex } from './departed.js';
7
7
  import { beginFade, leaveVoice, parting } from './fade.js';
@@ -105,6 +105,13 @@ export class Mixer {
105
105
  reducedNow = false;
106
106
  /** Something was changed since the last sync, so the next frame may differ from this one. */
107
107
  stirred = false;
108
+ /**
109
+ * The host read `inert` as true since the last sync, so its frame loop may be asleep until a
110
+ * change wakes it; a host that never asks keeps no loop asleep on it.
111
+ */
112
+ asleep = false;
113
+ /** Some voice cued while the mix slept waits for the next sync to set its start. */
114
+ waking = false;
108
115
  /** Counts the changes `stir` records, which a frame's record of a probe is current only before. */
109
116
  stirs = 0;
110
117
  syncing = false;
@@ -328,7 +335,7 @@ export class Mixer {
328
335
  return typeof r === 'function' ? r() : r === true;
329
336
  }
330
337
  get band() {
331
- return this.opts.band ?? { on: 0.6, off: 0.4 };
338
+ return this.opts.band ?? STOCK_BAND;
332
339
  }
333
340
  cue(spec) {
334
341
  this.attached();
@@ -474,6 +481,12 @@ export class Mixer {
474
481
  get inert() {
475
482
  if (this.stirred)
476
483
  return false;
484
+ const inert = this.restful();
485
+ if (inert && !Number.isNaN(this.now))
486
+ this.asleep = true;
487
+ return inert;
488
+ }
489
+ restful() {
477
490
  // Standing still, a voice changes no pose but by its weight signal, or by an anchor or start
478
491
  // that host time may yet reach.
479
492
  if (this.pace?.stopped(this.u))
package/dist/motion.d.ts CHANGED
@@ -44,6 +44,11 @@ export type Moving<I, O, V extends Value> = Patch<I, O, void> & {
44
44
  * back at the latest frame too, however long it is before it is next read.
45
45
  */
46
46
  push(subject: I, velocity: V, at?: number): void;
47
+ /**
48
+ * Puts `subject` at `value` from voice time `at`, keeping its velocity, with `at` defaulting as
49
+ * `push`'s does: a spring or tween heads for its target from there, a glide coasts on from there.
50
+ */
51
+ set(subject: I, value: V, at?: number): void;
47
52
  };
48
53
  /**
49
54
  * The state behind a `'motion'` patch, for an engine that reads it directly; a patch another copy
@@ -90,6 +95,11 @@ export declare function spring<I, O, V extends Value = number>(writes: keyof O,
90
95
  * `ms`, and comes to rest `velocity · ms / 1000` past where it was released. `push` sets it moving
91
96
  * again from wherever it is. Closed form, so frame rate does not change where it stops.
92
97
  *
98
+ * With `bounds`, a coast that would stop past an edge springs back to it from where it crosses, at
99
+ * the speed it crosses at, on a critically damped spring of `stiffness`; one let go past an edge
100
+ * springs back from there, unless it coasts back inside. A `set` past an edge leaves the subject
101
+ * there, coasting on, until the next `push`, so a drag can hold it out.
102
+ *
93
103
  * @category patch
94
104
  */
95
105
  export declare function glide<I, O, V extends Value = number>(writes: keyof O, opts: Common<I, V> & {
@@ -97,7 +107,14 @@ export declare function glide<I, O, V extends Value = number>(writes: keyof O, o
97
107
  from: PerSubject<I, V>;
98
108
  /** The friction's time constant, ms. Default 325, a touch scroll's. */
99
109
  ms?: number;
100
- }): Moving<I, O, V>;
110
+ /** The least and greatest value a coast comes to rest at, per axis. Default none. */
111
+ bounds?: PerSubject<I, readonly [min: V, max: V]>;
112
+ /** The spring back's, per second squared, with `bounds`. Default 170, as a spring's. */
113
+ stiffness?: number;
114
+ }): Moving<I, O, V> & {
115
+ /** `subject`'s bounds, undefined for a glide without them. */
116
+ bounds(subject: I): readonly [min: V, max: V] | undefined;
117
+ };
101
118
  /**
102
119
  * From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
103
120
  * change where a subject is. `to(subject, target)` retargets one subject mid-flight: the new stretch
package/dist/motion.js CHANGED
@@ -1,4 +1,4 @@
1
- import { COAST, CRITICAL, EASED, OVER, UNDER } from './closed.js';
1
+ import { BOUND, COAST, CRITICAL, EASED, OVER, UNDER } from './closed.js';
2
2
  import { curve } from './easing.js';
3
3
  import { Motions } from './motions.js';
4
4
  import { release } from './shared.js';
@@ -44,6 +44,9 @@ class MotionPatch {
44
44
  state.sample(s, setting.elapsed, state.xs, state.vs);
45
45
  return { [this.writes[0]]: state.value(s, state.xs) };
46
46
  }
47
+ set(subject, value, at) {
48
+ this.motions.change(subject, { at, x: axes(value) });
49
+ }
47
50
  read(subject, at) {
48
51
  const state = this.motions;
49
52
  const r = state.read(subject, at);
@@ -63,9 +66,18 @@ class SpringPatch extends TweenPatch {
63
66
  }
64
67
  }
65
68
  class GlidePatch extends MotionPatch {
69
+ edges;
70
+ constructor(writes, motions, edges) {
71
+ super(writes, motions);
72
+ this.edges = edges;
73
+ }
66
74
  push(subject, velocity, at) {
67
75
  this.motions.change(subject, { at, v: axes(velocity) });
68
76
  }
77
+ bounds(subject) {
78
+ const b = this.edges;
79
+ return b === undefined ? undefined : per(b, subject);
80
+ }
69
81
  }
70
82
  /**
71
83
  * The state behind a `'motion'` patch, for an engine that reads it directly; a patch another copy
@@ -124,12 +136,14 @@ class GlideShape {
124
136
  ms = undefined;
125
137
  start;
126
138
  speed;
139
+ edges;
127
140
  constructor(law, motion, opts, tau) {
128
141
  this.law = law;
129
142
  this.motion = motion;
130
143
  this.tau = tau;
131
144
  this.start = opts.from;
132
145
  this.speed = opts.velocity;
146
+ this.edges = opts.bounds;
133
147
  }
134
148
  spec() {
135
149
  return this.motion;
@@ -141,8 +155,24 @@ class GlideShape {
141
155
  const v = this.speed;
142
156
  return v === undefined ? still(x) : axes(per(v, s));
143
157
  }
144
- aim(x, v) {
145
- return x.map((xi, i) => xi + v[i] * this.tau);
158
+ aim(x, v, _was, s, put) {
159
+ const rest = x.map((xi, i) => xi + v[i] * this.tau);
160
+ if (this.edges === undefined)
161
+ return rest;
162
+ const [lo, hi] = per(this.edges, s);
163
+ const min = axes(lo);
164
+ const max = axes(hi);
165
+ if (min.length !== rest.length || max.length !== rest.length)
166
+ throw new Error('blits: bounds on a glide have as many axes as its subjects');
167
+ return rest.map((r, i) => {
168
+ const a = min[i];
169
+ const b = max[i];
170
+ const xi = x[i];
171
+ // A set past an edge stays there: a drag holds it out, and only a push lets it spring back.
172
+ if (put && (xi < a || xi > b))
173
+ return r;
174
+ return r < a ? a : r > b ? b : r;
175
+ });
146
176
  }
147
177
  scalar(s) {
148
178
  return typeof per(this.start, s) === 'number';
@@ -226,16 +256,25 @@ export function spring(writes, opts) {
226
256
  * `ms`, and comes to rest `velocity · ms / 1000` past where it was released. `push` sets it moving
227
257
  * again from wherever it is. Closed form, so frame rate does not change where it stops.
228
258
  *
259
+ * With `bounds`, a coast that would stop past an edge springs back to it from where it crosses, at
260
+ * the speed it crosses at, on a critically damped spring of `stiffness`; one let go past an edge
261
+ * springs back from there, unless it coasts back inside. A `set` past an edge leaves the subject
262
+ * there, coasting on, until the next `push`, so a drag can hold it out.
263
+ *
229
264
  * @category patch
230
265
  */
231
266
  export function glide(writes, opts) {
232
267
  const ms = opts.ms ?? 325;
233
268
  if (!(ms > 0))
234
269
  throw new RangeError('blits: a glide takes a positive ms');
270
+ const k = opts.stiffness ?? 170;
271
+ if (!(k > 0))
272
+ throw new RangeError('blits: a glide takes a positive stiffness');
235
273
  const tau = ms / 1000;
236
274
  const settle = opts.settle ?? 1e-4;
237
- const shape = new GlideShape([COAST, settle, tau, 0, 0], { kind: 'glide', ms, settle }, opts, tau);
238
- return new GlidePatch(writes, new Motions(shape));
275
+ const bounded = opts.bounds !== undefined;
276
+ const shape = new GlideShape(bounded ? [BOUND, settle, tau, Math.sqrt(k), 0] : [COAST, settle, tau, 0, 0], bounded ? { kind: 'glide', ms, settle, stiffness: k } : { kind: 'glide', ms, settle }, opts, tau);
277
+ return new GlidePatch(writes, new Motions(shape), opts.bounds);
239
278
  }
240
279
  /**
241
280
  * From one value to another over `ms` along an easing, solved in closed form, so frame rate does not
package/dist/motions.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface Change {
20
20
  */
21
21
  at?: number;
22
22
  to?: number[];
23
+ /** Where it moves from, in place of where its stretch has it then. */
24
+ x?: number[];
23
25
  v?: number[];
24
26
  /** The mix clock when it was made, NaN where no mix was playing the patch. */
25
27
  made?: number;
@@ -73,8 +75,11 @@ export interface Shape<I> {
73
75
  from: (subject: I) => number[];
74
76
  /** How fast each axis starts moving, given where it starts: still unless the options say. */
75
77
  velocity: (subject: I, x: number[]) => number[];
76
- /** Where it heads when released from `x` at `v`, given where it was heading. */
77
- aim: (x: number[], v: number[], was: number[] | null, subject: I) => number[];
78
+ /**
79
+ * Where it heads when released from `x` at `v`, given where it was heading; `put` for a stretch a
80
+ * `set` released.
81
+ */
82
+ aim: (x: number[], v: number[], was: number[] | null, subject: I, put: boolean) => number[];
78
83
  /** `[form, settle, k1, k2, k3]`. */
79
84
  law: readonly number[];
80
85
  /** Whether a subject's value is a number rather than an array. */
@@ -238,7 +243,7 @@ export declare class Motions<I> {
238
243
  s: number;
239
244
  } | undefined;
240
245
  /**
241
- * Queues a retarget or push, untimed at the latest frame, for `subject`'s next read past its time.
246
+ * Queues a retarget, set or push, untimed at the latest frame, for `subject`'s next read past its time.
242
247
  * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
243
248
  */
244
249
  change(subject: I, c: Change): void;
package/dist/motions.js CHANGED
@@ -126,7 +126,7 @@ export class Motions {
126
126
  return known;
127
127
  const x = this.shape.from(subject);
128
128
  const v = this.shape.velocity(subject, x);
129
- const to = this.shape.aim(x, v, null, subject);
129
+ const to = this.shape.aim(x, v, null, subject, false);
130
130
  const n = this.n < 0 ? x.length : this.n;
131
131
  this.check(x, n);
132
132
  this.check(v, n);
@@ -465,13 +465,14 @@ export class Motions {
465
465
  return { x, v, s };
466
466
  }
467
467
  /**
468
- * Queues a retarget or push, untimed at the latest frame, for `subject`'s next read past its time.
468
+ * Queues a retarget, set or push, untimed at the latest frame, for `subject`'s next read past its time.
469
469
  * The queue keeps the changes with a time in time order, ahead of any still waiting for one.
470
470
  */
471
471
  change(subject, c) {
472
472
  const back = this.owner === null ? Number.NaN : this.owner.revive(this.ownerId, subject);
473
473
  const s = this.slot(subject);
474
474
  this.check(c.to, this.n);
475
+ this.check(c.x, this.n);
475
476
  this.check(c.v, this.n);
476
477
  c.made = this.owner === null ? Number.NaN : this.owner.now();
477
478
  c.seq = this.owner === null ? Number.NaN : this.owner.seq();
@@ -494,7 +495,7 @@ export class Motions {
494
495
  else
495
496
  this.queue(list, c, c.at);
496
497
  reading.moved++;
497
- const made = { at: c.at, to: c.to, v: c.v };
498
+ const made = { at: c.at, to: c.to, x: c.x, v: c.v };
498
499
  this.owner?.changed(this.ownerId, this, subject, made);
499
500
  }
500
501
  /** Files a change with a time after every one due by then, ahead of those still waiting for one. */
@@ -671,11 +672,12 @@ export class Motions {
671
672
  landing = { x: new Float64Array(n), v: new Float64Array(n) };
672
673
  const { x, v } = landing;
673
674
  this.evaluateSegment(seg, at, x, v);
674
- const xs = numbers(x, 0, n);
675
+ const xs = change.x ?? numbers(x, 0, n);
675
676
  const vs = change.v ?? numbers(v, 0, n);
676
677
  const subject = this.numbers.subject(s);
677
678
  const known = subject === absent ? undefined : subject;
678
- const to = this.shape.aim(xs, vs, change.to ?? seg.to, known);
679
+ const put = change.x !== undefined && change.v === undefined;
680
+ const to = this.shape.aim(xs, vs, change.to ?? seg.to, known, put);
679
681
  const ms = subject === absent ? seg.ms : (this.shape.ms?.(known) ?? 0);
680
682
  return { at, ms, x0: xs, v0: vs, to, change };
681
683
  }
package/dist/move.js CHANGED
@@ -9,7 +9,7 @@ import { begin } from './held.js';
9
9
  import { noted } from './history.js';
10
10
  import { leave as leaveClock } from './origin.js';
11
11
  import { ownerReading } from './owner.js';
12
- import { mixAt, place, repin, startOf } from './place.js';
12
+ import { mixAt, place, repin, startAt, startOf } from './place.js';
13
13
  import { pageVoice } from './revive.js';
14
14
  import { delist } from './roster.js';
15
15
  import { scoredLeft, scoredRemove } from './scored.js';
@@ -47,6 +47,8 @@ function moveTo(mix, now) {
47
47
  for (const a of mix.announced)
48
48
  if (Number.isNaN(a.at))
49
49
  a.at = mix.u;
50
+ if (mix.waking)
51
+ wake(mix, now);
50
52
  if (mix.pins !== null && mix.pins.size > 0)
51
53
  repin(mix);
52
54
  if (mix.anchored > 0)
@@ -137,10 +139,31 @@ function moveTo(mix, now) {
137
139
  if (mix.sharers.length > 0 && !mix.projecting)
138
140
  outgrow(mix);
139
141
  mix.stirred = false;
142
+ mix.asleep = false;
140
143
  if (mix.retired.length > 0)
141
144
  prune(mix);
142
145
  forget(mix);
143
146
  }
147
+ /** Starts each voice cued while the mix slept at `now`, the first sync after its cue. */
148
+ function wake(mix, now) {
149
+ mix.waking = false;
150
+ for (const voice of mix.cued) {
151
+ if (!voice.waking)
152
+ continue;
153
+ voice.waking = false;
154
+ if (voice.state !== 'pending')
155
+ continue;
156
+ startAt(mix, voice, now);
157
+ // Where a seek back before the cue puts it, so the tape's cue again starts it here.
158
+ const first = voice.first;
159
+ if (first !== null) {
160
+ first.start = now;
161
+ first.anchorNow = now;
162
+ }
163
+ if (!mix.projecting)
164
+ schedule(mix, voice);
165
+ }
166
+ }
144
167
  /**
145
168
  * Which way a voice's clock is running at mix time `now`, through every owner above it: above 0
146
169
  * forward, below 0 back, 0 standing still.
@@ -172,6 +195,8 @@ function turn(mix, voice, now) {
172
195
  */
173
196
  const SEARCHED = 8;
174
197
  const SPARSE = 32;
198
+ /** The most retired voices `prune` splices out one at a time, rather than closing up in one pass. */
199
+ const SPLICED = 4;
175
200
  /**
176
201
  * Takes the voices retired since the last prune out of the list, in list order. A few, or a small
177
202
  * share of a long list, are found by their id, which the list is in order of, so the voices still
@@ -200,16 +225,21 @@ function prune(mix) {
200
225
  if (at !== null) {
201
226
  for (const voice of retired)
202
227
  pruned = leave(mix, voice) || pruned;
203
- // Closed up in one pass from the first to go: a splice for each moves the tail once a voice.
204
- let kept = at[0];
205
- let k = 0;
206
- for (let i = kept; i < voices.length; i++) {
207
- if (at[k] === i)
208
- k++;
209
- else
210
- voices[kept++] = voices[i];
228
+ // A splice moves the tail natively, several times faster than a loop, but once a voice.
229
+ if (at.length <= SPLICED)
230
+ for (let k = at.length - 1; k >= 0; k--)
231
+ voices.splice(at[k], 1);
232
+ else {
233
+ let kept = at[0];
234
+ let k = 0;
235
+ for (let i = kept; i < voices.length; i++) {
236
+ if (at[k] === i)
237
+ k++;
238
+ else
239
+ voices[kept++] = voices[i];
240
+ }
241
+ voices.length = kept;
211
242
  }
212
- voices.length = kept;
213
243
  }
214
244
  else {
215
245
  let kept = 0;
package/dist/origin.d.ts CHANGED
@@ -5,12 +5,14 @@ export interface Past extends Clock {
5
5
  until: number;
6
6
  /** The voice's `rebuilds` while it ran on this clock. */
7
7
  rebuilds: number;
8
+ /** Whether the voice left it by a seek. */
9
+ sought: boolean;
8
10
  }
9
11
  /**
10
12
  * Keeps the clock a voice is leaving at its local time `u`, ahead of a rate change, ramp or seek,
11
13
  * where a subject's origin could still be found on it.
12
14
  */
13
- export declare function leave<I, O>(voice: Voice<I, O>, u: number): void;
15
+ export declare function leave<I, O>(voice: Voice<I, O>, u: number, sought?: boolean): void;
14
16
  /**
15
17
  * The mix time a voice's clock first read `elapsed`, or will by its controls now, through its owners'
16
18
  * clocks. A clock entered already past it, by a seek, answers where that clock would have read it.
package/dist/origin.js CHANGED
@@ -5,7 +5,7 @@ const MOST = 256;
5
5
  * Keeps the clock a voice is leaving at its local time `u`, ahead of a rate change, ramp or seek,
6
6
  * where a subject's origin could still be found on it.
7
7
  */
8
- export function leave(voice, u) {
8
+ export function leave(voice, u, sought = false) {
9
9
  const past = voice.clocks;
10
10
  if (past !== null && past.length >= MOST)
11
11
  return;
@@ -14,7 +14,7 @@ export function leave(voice, u) {
14
14
  if (past !== null && reached(past, 0) >= reach && reached(past, fresh(voice)) >= reach)
15
15
  return;
16
16
  const { anchorNow, anchorElapsed, rate, ramp, rebuilds } = voice;
17
- const left = { anchorNow, anchorElapsed, rate, ramp, until: u, rebuilds };
17
+ const left = { anchorNow, anchorElapsed, rate, ramp, until: u, rebuilds, sought };
18
18
  if (past === null)
19
19
  voice.clocks = [left];
20
20
  else
@@ -41,6 +41,18 @@ function fresh(voice) {
41
41
  i++;
42
42
  return i;
43
43
  }
44
+ /**
45
+ * Where a walk for a stateful record at `elapsed` starts after a seek back before it: the clock that
46
+ * seek entered, since a subject sent back before its start starts afresh.
47
+ */
48
+ function backFrom(voice, past, elapsed) {
49
+ for (let i = past.length - 1; i >= 0; i--) {
50
+ const p = past[i];
51
+ if (p.sought && (past[i + 1]?.anchorElapsed ?? voice.anchorElapsed) < elapsed)
52
+ return i + 1;
53
+ }
54
+ return 0;
55
+ }
44
56
  /**
45
57
  * The mix time a voice's clock first read `elapsed`, or will by its controls now, through its owners'
46
58
  * clocks. A clock entered already past it, by a seek, answers where that clock would have read it.
@@ -51,7 +63,8 @@ export function originOf(voice, elapsed, rebuilt) {
51
63
  // A voice with no first pass shows every subject from its start, whatever its stagger.
52
64
  let t = voice.beginless ? voice.start : Number.NaN;
53
65
  if (past !== null && Number.isNaN(t)) {
54
- for (let i = rebuilt ? fresh(voice) : 0; i < past.length; i++) {
66
+ const from = rebuilt ? Math.max(fresh(voice), backFrom(voice, past, elapsed)) : 0;
67
+ for (let i = from; i < past.length; i++) {
55
68
  const p = past[i];
56
69
  const at = timeWith(p, elapsed);
57
70
  if (at <= p.until) {
package/dist/pack.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface PackedRecord {
7
7
  shown: number;
8
8
  weight: number;
9
9
  rested: boolean;
10
+ restedAt: number;
10
11
  bands: number[] | null;
11
12
  state: unknown;
12
13
  stepped: number;
package/dist/pack.js CHANGED
@@ -11,6 +11,7 @@ export function packHeld(voice, h) {
11
11
  shown: h.shown,
12
12
  weight: h.weight,
13
13
  rested: h.rested,
14
+ restedAt: h.restedAt,
14
15
  bands: h.bands === null ? null : Array.from(h.bands),
15
16
  state: h.state === undefined ? undefined : patch.pack ? patch.pack(h.state) : clone(h.state),
16
17
  stepped: h.stepped,
@@ -40,6 +41,7 @@ export function unpackHeld(voice, p) {
40
41
  h.shown = p.shown;
41
42
  h.weight = p.weight;
42
43
  h.rested = p.rested;
44
+ h.restedAt = p.restedAt;
43
45
  h.bands = bands;
44
46
  h.stepped = p.stepped;
45
47
  h.ticks = p.ticks;
package/dist/project.js CHANGED
@@ -291,6 +291,7 @@ function copyAhead(mix, c) {
291
291
  pin(c, copy, at);
292
292
  return copy;
293
293
  });
294
+ c.waking = mix.waking;
294
295
  ownBlends(c.cued);
295
296
  // A copy keeps no list of owners, so the one before says only by its voices.
296
297
  if (mix.owners !== null || (mix.projecting && c.cued.some((v) => v.holding !== null)))
package/dist/record.js CHANGED
@@ -13,6 +13,7 @@ export function record(voice, reaches, state) {
13
13
  shown: 0,
14
14
  weight: 0,
15
15
  rested: false,
16
+ restedAt: Number.NaN,
16
17
  bands: null,
17
18
  state,
18
19
  // -0, not 0: a field first given a small integer is boxed on every later write of a double.
package/dist/rows.js CHANGED
@@ -181,8 +181,10 @@ function copyRow(from, p, to, q, keys = from.keys) {
181
181
  carry(from.data, p * 8 /* Row.STRIDE */, to.data, q * 8 /* Row.STRIDE */, 8 /* Row.STRIDE */);
182
182
  carry(from.hot, p * from.stride, to.hot, q * to.stride, from.stride);
183
183
  copyKeys(keys, to, q);
184
- if ((from.hot[p * from.stride + 0 /* Hot.FLAGS */] & 4 /* Flag.STALE */) !== 0)
184
+ if ((from.hot[p * from.stride + 0 /* Hot.FLAGS */] & 4 /* Flag.STALE */) !== 0) {
185
185
  to.stale = true;
186
+ to.staleRows.push(q);
187
+ }
186
188
  carry(from.samples, p * from.axes, to.samples, q * to.axes, from.axes);
187
189
  to.deltas[q] = from.deltas[p] ?? null;
188
190
  to.records[q] = from.records[p];
package/dist/seek.js CHANGED
@@ -33,8 +33,7 @@ function restore(mix, voice, live, at, every, mine) {
33
33
  const h = mine && live.reaches ? unread(live) : copyHeld(voice, live);
34
34
  if (h.stepped > t)
35
35
  h.stepped = t;
36
- // Whether it had rested by `t` is not kept; the next probe finds it again.
37
- h.rested = false;
36
+ restedBy(h, live, t);
38
37
  h.weight = 0;
39
38
  h.inputs = inputs;
40
39
  if (!(h.since <= t))
@@ -63,6 +62,7 @@ function restore(mix, voice, live, at, every, mine) {
63
62
  else
64
63
  h = copyHeld(voice, snap.held, true);
65
64
  h.weight = 0;
65
+ restedBy(h, live, t);
66
66
  if (snap.at < t)
67
67
  h.copied = true;
68
68
  const kept = i + 1;
@@ -108,6 +108,14 @@ function restore(mix, voice, live, at, every, mine) {
108
108
  }
109
109
  return h;
110
110
  }
111
+ /**
112
+ * Whether a record put back for mix time `t` had rested by then, from when the live one did: a
113
+ * subject may have rested and moved on since, so a probe at `t` would not find it at rest again.
114
+ */
115
+ function restedBy(h, live, t) {
116
+ h.rested = live.restedAt <= t;
117
+ h.restedAt = h.rested ? live.restedAt : Number.NaN;
118
+ }
111
119
  /** Cuts a record's list of copies to its first `kept`, keeping those no other list holds as spares. */
112
120
  function trim(voice, snaps, kept) {
113
121
  while (snaps.length > kept) {
@@ -252,7 +260,14 @@ export function seek(transport, to) {
252
260
  if (frame === undefined) {
253
261
  // Ahead, under the rate the recorded calls up to `t` set, past any frames rate 0 held at `t`.
254
262
  const last = replay(transport, (at) => (transport.pace === null ? at : transport.pace.reading(at)) <= t);
255
- const at = hostAhead(transport, t);
263
+ const left = transport.left;
264
+ // Inverting the rate can land an ulp from where the host stood, which every later sync would carry.
265
+ const at = left !== null &&
266
+ left.t === t &&
267
+ left.u >= transport.u &&
268
+ (transport.pace === null ? left.u : transport.pace.reading(left.u)) === t
269
+ ? left.u
270
+ : hostAhead(transport, t);
256
271
  const u = Number.isNaN(last) ? at : Math.max(last, at);
257
272
  if (!Number.isFinite(u))
258
273
  throw new RangeError(`blits: the mix's rate never reaches ${t}`);
@@ -266,6 +281,7 @@ export function seek(transport, to) {
266
281
  // On the frame itself, its own host time: rate 0 may have held the clock there for several.
267
282
  const u = frame.at === t ? frame.u : hostAt(transport, t);
268
283
  const seq = frame.seq;
284
+ transport.left = { t: transport.now, u: transport.u };
269
285
  tape.goto(tape.depthAt(u));
270
286
  transport.undrop(seq);
271
287
  // What history kept after the frame is let go; the tape holds the calls that made it.
package/dist/shared.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export declare const release = "0.12.1";
5
+ export declare const release = "0.14.0";
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/shared.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * The package version, for a fact whose shape follows the code too closely to version by hand:
3
3
  * copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
4
4
  */
5
- export const release = '0.12.1';
5
+ export const release = '0.14.0';
6
6
  /**
7
7
  * A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
8
8
  * patch or channel it made is read by another copy's mix. The name carries a version, bumped when
package/dist/stack.d.ts CHANGED
@@ -3,8 +3,8 @@ import type { Kit } from './types.js';
3
3
  * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
4
4
  * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
5
5
  * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
6
- * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
7
- * a mix's default band first switches one on, and stays undefined where none is.
6
+ * channel's bounds. A channel with no rest takes the last delta a mix's default band switches on, any
7
+ * weight above 0, and stays undefined where none is.
8
8
  *
9
9
  * @category channel
10
10
  */
package/dist/stack.js CHANGED
@@ -1,13 +1,12 @@
1
+ import { STOCK_BAND } from './channels.js';
1
2
  import { copy, first } from './clone.js';
2
3
  import { bound, merged } from './fold.js';
3
- /** The weight a mix's default band first switches a rest-less channel on at. */
4
- const ON = 0.6;
5
4
  /**
6
5
  * Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
7
6
  * gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
8
7
  * weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
9
- * channel's bounds. A channel with no rest takes the last delta whose weight is 0.6 or more, where
10
- * a mix's default band first switches one on, and stays undefined where none is.
8
+ * channel's bounds. A channel with no rest takes the last delta a mix's default band switches on, any
9
+ * weight above 0, and stays undefined where none is.
11
10
  *
12
11
  * @category channel
13
12
  */
@@ -34,7 +33,7 @@ export function fold(kit, deltas, weights) {
34
33
  pose[key] = channel.fold
35
34
  ? channel.fold(pose[key], value, w)
36
35
  : merged(channel, pose[key], channel.scale(value, w));
37
- else if (w >= ON)
36
+ else if (w >= STOCK_BAND.on)
38
37
  pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
39
38
  }
40
39
  }
package/dist/strays.js CHANGED
@@ -117,7 +117,7 @@ function changeAhead(to, id, motion, given, change) {
117
117
  made.set(motion, bySubject);
118
118
  }
119
119
  const list = bySubject.get(subject);
120
- const c = { at, to: change.to, v: change.v };
120
+ const c = { at, to: change.to, x: change.x, v: change.v };
121
121
  if (list === undefined)
122
122
  bySubject.set(subject, [c]);
123
123
  else {
@@ -73,6 +73,11 @@ export declare class Transport implements TransportApi {
73
73
  /** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
74
74
  u: number;
75
75
  offset: number;
76
+ /** Where the last seek back left from, so a seek ahead to the same mix time lands on the same host time. */
77
+ left: {
78
+ t: number;
79
+ u: number;
80
+ } | null;
76
81
  /** The mix time of the first sync. */
77
82
  born: number;
78
83
  /** The earliest mix time what history keeps in memory restores exactly. */
package/dist/transport.js CHANGED
@@ -45,6 +45,8 @@ export class Transport {
45
45
  /** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
46
46
  u = Number.NaN;
47
47
  offset = 0;
48
+ /** Where the last seek back left from, so a seek ahead to the same mix time lands on the same host time. */
49
+ left = null;
48
50
  /** The mix time of the first sync. */
49
51
  born = Number.NaN;
50
52
  /** The earliest mix time what history keeps in memory restores exactly. */