@vectoriox/iox-ui 5.74.6 → 5.75.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.
@@ -86,9 +86,22 @@ function framePosition(played, count) {
86
86
  }
87
87
  /** On a Video whose pictures are drawing: the poster has done its job and steps aside. */
88
88
  const FRAMES_SHOWING_ATTRIBUTE = 'data-iox-frames-showing';
89
- /** How long a stopped scroll waits before settling on a whole picture, and how fast it gets there per frame. */
90
- const REST_AFTER_MS = 140;
91
- const REST_RATE = 0.25;
89
+ /**
90
+ * Below this many pictures a second the film stops following the scroll exactly and lands on its own. A landing scroll
91
+ * slows exponentially, so the last picture or two would take most of a second as one slow dissolve between two places:
92
+ * the subject showing twice, and then settling. Landing at least this fast keeps the end of the move a move.
93
+ */
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. */
96
+ const FILM_LANDING_DECAY = 5;
97
+ /** How quickly the film catches up with a scroll that turned back while it was landing ahead of it (per second). */
98
+ const FILM_CATCH_RATE = 25;
99
+ /** How quickly the film's reading of the scroll's speed follows it (per second). */
100
+ const FILM_SPEED_RATE = 30;
101
+ /** A step this short is the same frame asked twice: it moves nothing and reads no speed. */
102
+ const FILM_SAME_FRAME_SECONDS = 0.002;
103
+ const FILM_MAX_STEP_SECONDS = 0.1;
104
+ const FILM_EPSILON = 1e-6;
92
105
  /** How finely two neighbours mix. Finer than the eye reads, and a scroll that moves less than a step draws nothing. */
93
106
  const FRAME_BLEND_STEPS = 32;
94
107
  /**
@@ -105,6 +118,42 @@ function frameBlendAt(loaded, position) {
105
118
  const nearest = nearestLoadedFrame(loaded, Math.round(position));
106
119
  return nearest < 0 ? null : { from: nearest, to: nearest, mix: 0 };
107
120
  }
121
+ /** Where a film stands before its first step: on the moment asked for, still. */
122
+ function filmAt(wanted) {
123
+ return { position: wanted, wanted, speed: 0, direction: 1 };
124
+ }
125
+ /**
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.
129
+ */
130
+ function stepFilm(motion, wanted, seconds, last) {
131
+ const sameFrame = seconds < FILM_SAME_FRAME_SECONDS;
132
+ const step = Math.min(FILM_MAX_STEP_SECONDS, seconds);
133
+ const moved = wanted - motion.wanted;
134
+ 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
+ // Moving: with the scroll, but never back over a picture a landing already reached unless the scroll turned.
137
+ if (Math.abs(speed) >= FILM_LANDING_SPEED) {
138
+ const position = direction > 0 ? Math.max(wanted, Math.min(motion.position, Math.ceil(wanted))) : Math.min(wanted, Math.max(motion.position, Math.floor(wanted)));
139
+ return { position, wanted, speed, direction };
140
+ }
141
+ if (sameFrame)
142
+ return { ...motion, wanted, direction };
143
+ 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));
147
+ const behind = (wanted - motion.position) * direction;
148
+ const reach = Math.max(FILM_LANDING_SPEED * step, behind > 0 ? behind * (1 - Math.exp(-FILM_CATCH_RATE * step)) : 0);
149
+ const position = direction > 0 ? Math.min(goal, motion.position + reach) : Math.max(goal, motion.position - reach);
150
+ return { position, wanted, speed, direction };
151
+ }
152
+ /** Whether a film has landed: still, on the whole picture at or just past where the scroll stands. */
153
+ 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;
156
+ }
108
157
  /** The picture at `played` % of a set of `count`. */
109
158
  function frameIndexAt(played, count) {
110
159
  if (count <= 1)
@@ -199,12 +248,12 @@ class FramePlayer {
199
248
  this.el = el;
200
249
  this.sequence = sequence;
201
250
  this.played = null;
202
- this.restTimer = null;
203
- this.restFrame = null;
251
+ this.motion = null;
252
+ this.steppedAt = 0;
253
+ this.landingFrame = null;
204
254
  this.observer = null;
205
255
  this.canvas = null;
206
256
  this.context = null;
207
- this.position = 0;
208
257
  this.drawn = null;
209
258
  this.nextToLoad = 0;
210
259
  this.loading = 0;
@@ -235,34 +284,27 @@ class FramePlayer {
235
284
  if (played === this.played)
236
285
  return;
237
286
  this.played = played;
238
- this.stopResting();
239
- this.position = framePosition(played, this.info.count);
287
+ this.step();
288
+ }
289
+ /** One step toward the moment asked for, drawn now; a landing goes on by itself until it stands on a whole picture. */
290
+ step() {
291
+ if (this.destroyed || this.played === null)
292
+ return;
293
+ 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);
296
+ this.steppedAt = now;
240
297
  this.draw();
241
- this.restTimer = this.host.setTimeout?.(() => this.rest(), REST_AFTER_MS) ?? null;
242
- }
243
- /** A stopped scroll must not stand on two pictures at once: a moving subject would show twice. It settles on the nearer. */
244
- rest() {
245
- this.restTimer = null;
246
- const target = Math.round(this.position);
247
- const step = () => {
248
- this.restFrame = null;
249
- if (this.destroyed)
250
- return;
251
- const left = target - this.position;
252
- this.position = Math.abs(left) < 1 / FRAME_BLEND_STEPS ? target : this.position + left * REST_RATE;
253
- this.draw();
254
- if (this.position !== target)
255
- this.restFrame = this.host.requestAnimationFrame(step);
256
- };
257
- step();
298
+ if (!filmLanded(this.motion) && this.landingFrame === null)
299
+ this.landingFrame = this.host.requestAnimationFrame(() => {
300
+ this.landingFrame = null;
301
+ this.step();
302
+ });
258
303
  }
259
- stopResting() {
260
- if (this.restTimer !== null)
261
- this.host.clearTimeout?.(this.restTimer);
262
- if (this.restFrame !== null)
263
- this.host.cancelAnimationFrame?.(this.restFrame);
264
- this.restTimer = null;
265
- this.restFrame = null;
304
+ stopLanding() {
305
+ if (this.landingFrame !== null)
306
+ this.host.cancelAnimationFrame?.(this.landingFrame);
307
+ this.landingFrame = null;
266
308
  }
267
309
  /** Whether a re-render took the canvas away with the markup it sat in. */
268
310
  get detached() {
@@ -270,7 +312,7 @@ class FramePlayer {
270
312
  }
271
313
  destroy() {
272
314
  this.destroyed = true;
273
- this.stopResting();
315
+ this.stopLanding();
274
316
  this.el.removeAttribute(FRAMES_SHOWING_ATTRIBUTE);
275
317
  this.observer?.disconnect();
276
318
  this.observer = null;
@@ -330,7 +372,9 @@ class FramePlayer {
330
372
  image.src = frameUrl(this.sequence, this.set, index);
331
373
  }
332
374
  draw() {
333
- const blend = frameBlendAt(this.images, this.position);
375
+ if (!this.motion)
376
+ return;
377
+ const blend = frameBlendAt(this.images, this.motion.position);
334
378
  const last = this.drawn;
335
379
  if (!blend || (last && last.from === blend.from && last.to === blend.to && last.mix === blend.mix))
336
380
  return;
@@ -988,5 +1032,5 @@ function videoRuntimeOf(el) {
988
1032
  * Generated bundle index. Do not edit.
989
1033
  */
990
1034
 
991
- export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FRAMES_PHONE_MAX_WIDTH, FRAMES_SHOWING_ATTRIBUTE, FRAME_BLEND_STEPS, FRAME_SEQUENCE_ID, FrameExtension, FramePlayer, FrameSet, FramesClass, MAX_FRAMES, MAX_FRAME_BYTES, PHONE_LONG_SIDE, VIDEO_EMBED_ORIGINS, VIDEO_NODE_TYPE, VIDEO_PROVIDER_COMPANY, VIDEO_PROVIDER_LABEL, VIDEO_WORDS, VideoAttribute, VideoClass, VideoIcon, VideoKind, VideoMode, VideoRuntime, VideoSource, VideoTrait, WIDE_LONG_SIDE, attachVideoAt, attachVideos, documentConsentTransport, fittedFrameSize, frameBlendAt, frameFileName, frameIndexAt, frameLoadOrder, framePosition, frameSetFor, frameSummary, frameUrl, isThirdPartyPoster, nearestLoadedFrame, normalizeFrameSequence, parseVideoUrl, phoneFrameCount, phoneFrameSrcset, planVideo, readConsentRecord, sampledFrameIndexes, serializeVideoAttributes, sortFrameFiles, videoConsentCategory, videoEmbedUrl, videoFrameCount, videoInnerHtml, videoPlayLabel, videoPosterFileName, videoProp, videoRefOf, videoRootAttributes, videoRuntimeOf, videoSourceOf, videoThumbnailRequest, videoWords };
1035
+ export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FILM_LANDING_SPEED, FRAMES_PHONE_MAX_WIDTH, FRAMES_SHOWING_ATTRIBUTE, FRAME_BLEND_STEPS, FRAME_SEQUENCE_ID, FrameExtension, FramePlayer, FrameSet, FramesClass, MAX_FRAMES, MAX_FRAME_BYTES, PHONE_LONG_SIDE, VIDEO_EMBED_ORIGINS, VIDEO_NODE_TYPE, VIDEO_PROVIDER_COMPANY, VIDEO_PROVIDER_LABEL, VIDEO_WORDS, VideoAttribute, VideoClass, VideoIcon, VideoKind, VideoMode, VideoRuntime, VideoSource, VideoTrait, WIDE_LONG_SIDE, attachVideoAt, attachVideos, documentConsentTransport, filmAt, filmLanded, fittedFrameSize, frameBlendAt, frameFileName, frameIndexAt, frameLoadOrder, framePosition, frameSetFor, frameSummary, frameUrl, isThirdPartyPoster, nearestLoadedFrame, normalizeFrameSequence, parseVideoUrl, phoneFrameCount, phoneFrameSrcset, planVideo, readConsentRecord, sampledFrameIndexes, serializeVideoAttributes, sortFrameFiles, stepFilm, videoConsentCategory, videoEmbedUrl, videoFrameCount, videoInnerHtml, videoPlayLabel, videoPosterFileName, videoProp, videoRefOf, videoRootAttributes, videoRuntimeOf, videoSourceOf, videoThumbnailRequest, videoWords };
992
1036
  //# sourceMappingURL=vectoriox-iox-ui-media-core.mjs.map