@vectoriox/iox-ui 5.74.0 → 5.74.2

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.
@@ -108,6 +108,13 @@ export function framePosition(played: number, count: number): number {
108
108
  return (Math.min(100, Math.max(0, played)) / 100) * (count - 1);
109
109
  }
110
110
 
111
+ /** On a Video whose pictures are drawing: the poster has done its job and steps aside. */
112
+ export const FRAMES_SHOWING_ATTRIBUTE = 'data-iox-frames-showing';
113
+
114
+ /** How long a stopped scroll waits before settling on a whole picture, and how fast it gets there per frame. */
115
+ const REST_AFTER_MS = 140;
116
+ const REST_RATE = 0.25;
117
+
111
118
  /** How finely two neighbours mix. Finer than the eye reads, and a scroll that moves less than a step draws nothing. */
112
119
  export const FRAME_BLEND_STEPS = 32;
113
120
 
@@ -247,6 +254,9 @@ export interface FramePlayerOptions {
247
254
  * Browser only. It writes and never reads, so the driver can call it in its write phase.
248
255
  */
249
256
  export class FramePlayer {
257
+ private played: number | null = null;
258
+ private restTimer: number | null = null;
259
+ private restFrame: number | null = null;
250
260
  private readonly set: FrameSet;
251
261
  private readonly info: FrameSetInfo;
252
262
  private readonly images: Array<HTMLImageElement | null>;
@@ -284,8 +294,34 @@ export class FramePlayer {
284
294
 
285
295
  /** Stand at `played` %. */
286
296
  seek(played: number): void {
297
+ if (played === this.played) return;
298
+ this.played = played;
299
+ this.stopResting();
287
300
  this.position = framePosition(played, this.info.count);
288
301
  this.draw();
302
+ this.restTimer = this.host.setTimeout?.(() => this.rest(), REST_AFTER_MS) ?? null;
303
+ }
304
+
305
+ /** A stopped scroll must not stand on two pictures at once: a moving subject would show twice. It settles on the nearer. */
306
+ private rest(): void {
307
+ this.restTimer = null;
308
+ const target = Math.round(this.position);
309
+ const step = (): void => {
310
+ this.restFrame = null;
311
+ if (this.destroyed) return;
312
+ const left = target - this.position;
313
+ this.position = Math.abs(left) < 1 / FRAME_BLEND_STEPS ? target : this.position + left * REST_RATE;
314
+ this.draw();
315
+ if (this.position !== target) this.restFrame = this.host.requestAnimationFrame(step);
316
+ };
317
+ step();
318
+ }
319
+
320
+ private stopResting(): void {
321
+ if (this.restTimer !== null) this.host.clearTimeout?.(this.restTimer);
322
+ if (this.restFrame !== null) this.host.cancelAnimationFrame?.(this.restFrame);
323
+ this.restTimer = null;
324
+ this.restFrame = null;
289
325
  }
290
326
 
291
327
  /** Whether a re-render took the canvas away with the markup it sat in. */
@@ -295,6 +331,8 @@ export class FramePlayer {
295
331
 
296
332
  destroy(): void {
297
333
  this.destroyed = true;
334
+ this.stopResting();
335
+ this.el.removeAttribute(FRAMES_SHOWING_ATTRIBUTE);
298
336
  this.observer?.disconnect();
299
337
  this.observer = null;
300
338
  for (const image of this.pending) {
@@ -368,6 +406,8 @@ export class FramePlayer {
368
406
  context.drawImage(this.images[blend.to]!, 0, 0, width, height);
369
407
  context.globalAlpha = 1;
370
408
  }
409
+ // Over a transparent film the poster would show through: a second, stiller subject behind the moving one.
410
+ if (!this.drawn) this.el.setAttribute(FRAMES_SHOWING_ATTRIBUTE, '');
371
411
  this.drawn = blend;
372
412
  }
373
413
 
@@ -310,6 +310,8 @@ export function applySceneFirstFrame(el: HTMLElement | null | undefined, node: S
310
310
  export const SCENE_FLOW_RULES = `.iox-role-scene{height:auto!important}`
311
311
  + `.iox-role-scene-stage{position:relative!important;top:auto!important;height:auto!important;overflow:visible!important;container-type:normal!important}`
312
312
  + `[${SceneAttribute.Tracks}]{transform:none!important;opacity:1!important;clip-path:none!important;pointer-events:auto!important}`
313
+ // A row the scroll slid sideways has no scroll to slide it now: it wraps, or its last items run off the side of a phone.
314
+ + `[${SceneAttribute.Tracks}]{max-width:100%!important;flex-wrap:wrap!important}`
313
315
  // A Video the scroll plays stands as its poster, at the poster's own height: a percent of a stage that now flows is nothing.
314
316
  + `[${VideoAttribute.Frames}]{height:auto!important}`
315
317
  + `[${VideoAttribute.Frames}]>.${VideoClass.Poster},[${VideoAttribute.Frames}]>picture>.${VideoClass.Poster}{position:static!important;height:auto!important}`;
@@ -359,6 +359,7 @@ dialog[data-iox-dialog]:not([open]) {
359
359
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0; margin: 0; display: block; object-fit: cover;
360
360
  }
361
361
  .iox-video > .iox-video__poster, .iox-video > picture > .iox-video__poster { z-index: -1; }
362
+ .iox-video[data-iox-frames-showing] > .iox-video__poster, .iox-video[data-iox-frames-showing] > picture > .iox-video__poster { visibility: hidden; }
362
363
  .iox-video > .iox-video__media { object-fit: contain; background-color: var(--iox-inverse-bg); }
363
364
  .iox-video[data-iox-video-mode='background'] > .iox-video__media { object-fit: cover; }
364
365
  .iox-video__play {