@vectoriox/iox-ui 5.75.0 → 5.75.3

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.
@@ -92,7 +92,7 @@ const FRAMES_SHOWING_ATTRIBUTE = 'data-iox-frames-showing';
92
92
  * the subject showing twice, and then settling. Landing at least this fast keeps the end of the move a move.
93
93
  */
94
94
  const FILM_LANDING_SPEED = 10;
95
- /** How fast a landing scroll is assumed to slow (per second): where it will stop is its speed over this. */
95
+ /** With no word from the driver on where the scroll is going, how fast a landing scroll is assumed to slow (per second). */
96
96
  const FILM_LANDING_DECAY = 5;
97
97
  /** How quickly the film catches up with a scroll that turned back while it was landing ahead of it (per second). */
98
98
  const FILM_CATCH_RATE = 25;
@@ -124,15 +124,17 @@ function filmAt(wanted) {
124
124
  }
125
125
  /**
126
126
  * One step of the film. Moving fast, it stands exactly where the scroll asks. Landing (slower than FILM_LANDING_SPEED
127
- * pictures a second) it runs on at that speed to the whole picture past where the scroll will stop, and stands there: it
128
- * never crawls through a dissolve, never stops between two pictures, and never steps back unless the scroll does.
127
+ * pictures a second) it runs on at that speed to the whole picture nearest where the scroll will stop, and stands there:
128
+ * it never crawls through a dissolve, never stops between two pictures, and never steps back over a picture it reached
129
+ * unless the scroll turns. `aim` is where the scroll is heading, when the driver knows it (its undamped target); without it, it is guessed.
129
130
  */
130
- function stepFilm(motion, wanted, seconds, last) {
131
+ function stepFilm(motion, wanted, seconds, last, aim) {
131
132
  const sameFrame = seconds < FILM_SAME_FRAME_SECONDS;
132
133
  const step = Math.min(FILM_MAX_STEP_SECONDS, seconds);
133
134
  const moved = wanted - motion.wanted;
134
135
  const speed = sameFrame ? motion.speed : motion.speed + (moved / step - motion.speed) * (1 - Math.exp(-FILM_SPEED_RATE * step));
135
- const direction = Math.abs(moved) > FILM_EPSILON ? (moved > 0 ? 1 : -1) : motion.direction;
136
+ const heading = aim === undefined ? moved : aim - wanted;
137
+ const direction = Math.abs(heading) > FILM_EPSILON ? (heading > 0 ? 1 : -1) : Math.abs(moved) > FILM_EPSILON ? (moved > 0 ? 1 : -1) : motion.direction;
136
138
  // Moving: with the scroll, but never back over a picture a landing already reached unless the scroll turned.
137
139
  if (Math.abs(speed) >= FILM_LANDING_SPEED) {
138
140
  const position = direction > 0 ? Math.max(wanted, Math.min(motion.position, Math.ceil(wanted))) : Math.min(wanted, Math.max(motion.position, Math.floor(wanted)));
@@ -141,18 +143,19 @@ function stepFilm(motion, wanted, seconds, last) {
141
143
  if (sameFrame)
142
144
  return { ...motion, wanted, direction };
143
145
  const clamp = (value) => Math.min(last, Math.max(0, value));
144
- const stop = wanted + speed / FILM_LANDING_DECAY;
145
- const reached = direction > 0 ? Math.ceil(motion.position - FILM_EPSILON) : Math.floor(motion.position + FILM_EPSILON);
146
- const goal = clamp(direction > 0 ? Math.max(Math.ceil(stop - FILM_EPSILON), reached) : Math.min(Math.floor(stop + FILM_EPSILON), reached));
146
+ const stop = aim ?? wanted + speed / FILM_LANDING_DECAY;
147
+ // The picture nearest the stop, unless reaching it would show a step back: then the next whole one on the way.
148
+ const nearest = Math.round(stop);
149
+ const back = (motion.position - nearest) * direction > 1 / FRAME_BLEND_STEPS;
150
+ const goal = clamp(back ? (direction > 0 ? Math.ceil(motion.position) : Math.floor(motion.position)) : nearest);
147
151
  const behind = (wanted - motion.position) * direction;
148
152
  const reach = Math.max(FILM_LANDING_SPEED * step, behind > 0 ? behind * (1 - Math.exp(-FILM_CATCH_RATE * step)) : 0);
149
153
  const position = direction > 0 ? Math.min(goal, motion.position + reach) : Math.max(goal, motion.position - reach);
150
154
  return { position, wanted, speed, direction };
151
155
  }
152
- /** Whether a film has landed: still, on the whole picture at or just past where the scroll stands. */
156
+ /** Whether a film has landed: still, on a whole picture beside where the scroll stands. */
153
157
  function filmLanded(motion) {
154
- const past = (motion.position - motion.wanted) * motion.direction;
155
- return Math.abs(motion.speed) < 0.01 && Number.isInteger(motion.position) && past > -FILM_EPSILON && past < 1;
158
+ return Math.abs(motion.speed) < 0.01 && Number.isInteger(motion.position) && Math.abs(motion.position - motion.wanted) < 1;
156
159
  }
157
160
  /** The picture at `played` % of a set of `count`. */
158
161
  function frameIndexAt(played, count) {
@@ -279,26 +282,30 @@ class FramePlayer {
279
282
  }, { rootMargin: NEAR_MARGIN });
280
283
  this.observer.observe(options.near ?? el);
281
284
  }
282
- /** Stand at `played` %. */
283
- seek(played) {
284
- if (played === this.played)
285
+ /**
286
+ * Stand at `played` %, on the way to `aim` % when the caller knows where the scroll is going. `now` is the caller's
287
+ * frame time: the player keeps time by frames, so a frame asked twice moves once however long the page took.
288
+ */
289
+ seek(played, aim, now) {
290
+ if (played === this.played && aim === this.aim)
285
291
  return;
286
292
  this.played = played;
287
- this.step();
293
+ this.aim = aim;
294
+ this.step(now ?? this.host.performance?.now() ?? 0);
288
295
  }
289
296
  /** One step toward the moment asked for, drawn now; a landing goes on by itself until it stands on a whole picture. */
290
- step() {
297
+ step(now) {
291
298
  if (this.destroyed || this.played === null)
292
299
  return;
293
300
  const wanted = framePosition(this.played, this.info.count);
294
- const now = this.host.performance?.now() ?? Date.now();
295
- this.motion = this.motion ? stepFilm(this.motion, wanted, (now - this.steppedAt) / 1000, this.info.count - 1) : filmAt(wanted);
301
+ const aim = this.aim === undefined ? undefined : framePosition(this.aim, this.info.count);
302
+ this.motion = this.motion ? stepFilm(this.motion, wanted, (now - this.steppedAt) / 1000, this.info.count - 1, aim) : filmAt(wanted);
296
303
  this.steppedAt = now;
297
304
  this.draw();
298
305
  if (!filmLanded(this.motion) && this.landingFrame === null)
299
- this.landingFrame = this.host.requestAnimationFrame(() => {
306
+ this.landingFrame = this.host.requestAnimationFrame(time => {
300
307
  this.landingFrame = null;
301
- this.step();
308
+ this.step(time);
302
309
  });
303
310
  }
304
311
  stopLanding() {