@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.
- package/fesm2022/vectoriox-iox-ui-media-core.mjs +79 -35
- package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +6 -6
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/media-core/index.d.ts +35 -8
- package/package.json +1 -1
- package/src/media-core/frames.ts +88 -30
package/media-core/index.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ declare function frameSetFor(sequence: FrameSequence, phone: boolean): {
|
|
|
61
61
|
declare function framePosition(played: number, count: number): number;
|
|
62
62
|
/** On a Video whose pictures are drawing: the poster has done its job and steps aside. */
|
|
63
63
|
declare const FRAMES_SHOWING_ATTRIBUTE = "data-iox-frames-showing";
|
|
64
|
+
/**
|
|
65
|
+
* Below this many pictures a second the film stops following the scroll exactly and lands on its own. A landing scroll
|
|
66
|
+
* slows exponentially, so the last picture or two would take most of a second as one slow dissolve between two places:
|
|
67
|
+
* the subject showing twice, and then settling. Landing at least this fast keeps the end of the move a move.
|
|
68
|
+
*/
|
|
69
|
+
declare const FILM_LANDING_SPEED = 10;
|
|
64
70
|
/** How finely two neighbours mix. Finer than the eye reads, and a scroll that moves less than a step draws nothing. */
|
|
65
71
|
declare const FRAME_BLEND_STEPS = 32;
|
|
66
72
|
/** What to draw: picture `from`, with `to` over it at `mix` when the moment falls between two that have arrived. */
|
|
@@ -75,6 +81,27 @@ interface FrameBlend {
|
|
|
75
81
|
* A neighbour still loading never blends: the nearest arrived picture stands in, and null means none has.
|
|
76
82
|
*/
|
|
77
83
|
declare function frameBlendAt(loaded: ReadonlyArray<unknown>, position: number): FrameBlend | null;
|
|
84
|
+
/** Where the film stands and how the scroll under it moves: what one landing step reads and writes. */
|
|
85
|
+
interface FilmMotion {
|
|
86
|
+
/** The moment drawn, in pictures (2.5 = half way from the third to the fourth). */
|
|
87
|
+
position: number;
|
|
88
|
+
/** The moment the scroll asks for. */
|
|
89
|
+
wanted: number;
|
|
90
|
+
/** The scroll's speed, in pictures a second, smoothed. */
|
|
91
|
+
speed: number;
|
|
92
|
+
/** The way the scroll last moved: 1 onward, -1 back. */
|
|
93
|
+
direction: 1 | -1;
|
|
94
|
+
}
|
|
95
|
+
/** Where a film stands before its first step: on the moment asked for, still. */
|
|
96
|
+
declare function filmAt(wanted: number): FilmMotion;
|
|
97
|
+
/**
|
|
98
|
+
* One step of the film. Moving fast, it stands exactly where the scroll asks. Landing (slower than FILM_LANDING_SPEED
|
|
99
|
+
* pictures a second) it runs on at that speed to the whole picture past where the scroll will stop, and stands there: it
|
|
100
|
+
* never crawls through a dissolve, never stops between two pictures, and never steps back unless the scroll does.
|
|
101
|
+
*/
|
|
102
|
+
declare function stepFilm(motion: FilmMotion, wanted: number, seconds: number, last: number): FilmMotion;
|
|
103
|
+
/** Whether a film has landed: still, on the whole picture at or just past where the scroll stands. */
|
|
104
|
+
declare function filmLanded(motion: FilmMotion): boolean;
|
|
78
105
|
/** The picture at `played` % of a set of `count`. */
|
|
79
106
|
declare function frameIndexAt(played: number, count: number): number;
|
|
80
107
|
/**
|
|
@@ -130,8 +157,9 @@ declare class FramePlayer {
|
|
|
130
157
|
private readonly el;
|
|
131
158
|
private readonly sequence;
|
|
132
159
|
private played;
|
|
133
|
-
private
|
|
134
|
-
private
|
|
160
|
+
private motion;
|
|
161
|
+
private steppedAt;
|
|
162
|
+
private landingFrame;
|
|
135
163
|
private readonly set;
|
|
136
164
|
private readonly info;
|
|
137
165
|
private readonly images;
|
|
@@ -140,7 +168,6 @@ declare class FramePlayer {
|
|
|
140
168
|
private observer;
|
|
141
169
|
private canvas;
|
|
142
170
|
private context;
|
|
143
|
-
private position;
|
|
144
171
|
private drawn;
|
|
145
172
|
private nextToLoad;
|
|
146
173
|
private loading;
|
|
@@ -150,9 +177,9 @@ declare class FramePlayer {
|
|
|
150
177
|
constructor(el: HTMLElement, sequence: FrameSequence, options?: FramePlayerOptions);
|
|
151
178
|
/** Stand at `played` %. */
|
|
152
179
|
seek(played: number): void;
|
|
153
|
-
/**
|
|
154
|
-
private
|
|
155
|
-
private
|
|
180
|
+
/** One step toward the moment asked for, drawn now; a landing goes on by itself until it stands on a whole picture. */
|
|
181
|
+
private step;
|
|
182
|
+
private stopLanding;
|
|
156
183
|
/** Whether a re-render took the canvas away with the markup it sat in. */
|
|
157
184
|
get detached(): boolean;
|
|
158
185
|
destroy(): void;
|
|
@@ -445,5 +472,5 @@ declare function attachVideos(container: ParentNode, options?: VideoRuntimeOptio
|
|
|
445
472
|
/** The runtime already running at `el`, if any. */
|
|
446
473
|
declare function videoRuntimeOf(el: Element | null | undefined): VideoRuntime | null;
|
|
447
474
|
|
|
448
|
-
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 };
|
|
449
|
-
export type { ConsentGrantDetail, ConsentTransport, FrameBlend, FramePlayerOptions, FrameSequence, FrameSetInfo, FrameSizes, VideoMarkupOptions, VideoNodeLike, VideoPlan, VideoRef, VideoRuntimeOptions, VideoThumbnailRequest, VideoWords };
|
|
475
|
+
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 };
|
|
476
|
+
export type { ConsentGrantDetail, ConsentTransport, FilmMotion, FrameBlend, FramePlayerOptions, FrameSequence, FrameSetInfo, FrameSizes, VideoMarkupOptions, VideoNodeLike, VideoPlan, VideoRef, VideoRuntimeOptions, VideoThumbnailRequest, VideoWords };
|
package/package.json
CHANGED
package/src/media-core/frames.ts
CHANGED
|
@@ -111,9 +111,22 @@ export function framePosition(played: number, count: number): number {
|
|
|
111
111
|
/** On a Video whose pictures are drawing: the poster has done its job and steps aside. */
|
|
112
112
|
export const FRAMES_SHOWING_ATTRIBUTE = 'data-iox-frames-showing';
|
|
113
113
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Below this many pictures a second the film stops following the scroll exactly and lands on its own. A landing scroll
|
|
116
|
+
* slows exponentially, so the last picture or two would take most of a second as one slow dissolve between two places:
|
|
117
|
+
* the subject showing twice, and then settling. Landing at least this fast keeps the end of the move a move.
|
|
118
|
+
*/
|
|
119
|
+
export const FILM_LANDING_SPEED = 10;
|
|
120
|
+
/** How fast a landing scroll is assumed to slow (per second): where it will stop is its speed over this. */
|
|
121
|
+
const FILM_LANDING_DECAY = 5;
|
|
122
|
+
/** How quickly the film catches up with a scroll that turned back while it was landing ahead of it (per second). */
|
|
123
|
+
const FILM_CATCH_RATE = 25;
|
|
124
|
+
/** How quickly the film's reading of the scroll's speed follows it (per second). */
|
|
125
|
+
const FILM_SPEED_RATE = 30;
|
|
126
|
+
/** A step this short is the same frame asked twice: it moves nothing and reads no speed. */
|
|
127
|
+
const FILM_SAME_FRAME_SECONDS = 0.002;
|
|
128
|
+
const FILM_MAX_STEP_SECONDS = 0.1;
|
|
129
|
+
const FILM_EPSILON = 1e-6;
|
|
117
130
|
|
|
118
131
|
/** How finely two neighbours mix. Finer than the eye reads, and a scroll that moves less than a step draws nothing. */
|
|
119
132
|
export const FRAME_BLEND_STEPS = 32;
|
|
@@ -139,6 +152,56 @@ export function frameBlendAt(loaded: ReadonlyArray<unknown>, position: number):
|
|
|
139
152
|
return nearest < 0 ? null : { from: nearest, to: nearest, mix: 0 };
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
/** Where the film stands and how the scroll under it moves: what one landing step reads and writes. */
|
|
156
|
+
export interface FilmMotion {
|
|
157
|
+
/** The moment drawn, in pictures (2.5 = half way from the third to the fourth). */
|
|
158
|
+
position: number;
|
|
159
|
+
/** The moment the scroll asks for. */
|
|
160
|
+
wanted: number;
|
|
161
|
+
/** The scroll's speed, in pictures a second, smoothed. */
|
|
162
|
+
speed: number;
|
|
163
|
+
/** The way the scroll last moved: 1 onward, -1 back. */
|
|
164
|
+
direction: 1 | -1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Where a film stands before its first step: on the moment asked for, still. */
|
|
168
|
+
export function filmAt(wanted: number): FilmMotion {
|
|
169
|
+
return { position: wanted, wanted, speed: 0, direction: 1 };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One step of the film. Moving fast, it stands exactly where the scroll asks. Landing (slower than FILM_LANDING_SPEED
|
|
174
|
+
* pictures a second) it runs on at that speed to the whole picture past where the scroll will stop, and stands there: it
|
|
175
|
+
* never crawls through a dissolve, never stops between two pictures, and never steps back unless the scroll does.
|
|
176
|
+
*/
|
|
177
|
+
export function stepFilm(motion: FilmMotion, wanted: number, seconds: number, last: number): FilmMotion {
|
|
178
|
+
const sameFrame = seconds < FILM_SAME_FRAME_SECONDS;
|
|
179
|
+
const step = Math.min(FILM_MAX_STEP_SECONDS, seconds);
|
|
180
|
+
const moved = wanted - motion.wanted;
|
|
181
|
+
const speed = sameFrame ? motion.speed : motion.speed + (moved / step - motion.speed) * (1 - Math.exp(-FILM_SPEED_RATE * step));
|
|
182
|
+
const direction: 1 | -1 = Math.abs(moved) > FILM_EPSILON ? (moved > 0 ? 1 : -1) : motion.direction;
|
|
183
|
+
// Moving: with the scroll, but never back over a picture a landing already reached unless the scroll turned.
|
|
184
|
+
if (Math.abs(speed) >= FILM_LANDING_SPEED) {
|
|
185
|
+
const position = direction > 0 ? Math.max(wanted, Math.min(motion.position, Math.ceil(wanted))) : Math.min(wanted, Math.max(motion.position, Math.floor(wanted)));
|
|
186
|
+
return { position, wanted, speed, direction };
|
|
187
|
+
}
|
|
188
|
+
if (sameFrame) return { ...motion, wanted, direction };
|
|
189
|
+
const clamp = (value: number): number => Math.min(last, Math.max(0, value));
|
|
190
|
+
const stop = wanted + speed / FILM_LANDING_DECAY;
|
|
191
|
+
const reached = direction > 0 ? Math.ceil(motion.position - FILM_EPSILON) : Math.floor(motion.position + FILM_EPSILON);
|
|
192
|
+
const goal = clamp(direction > 0 ? Math.max(Math.ceil(stop - FILM_EPSILON), reached) : Math.min(Math.floor(stop + FILM_EPSILON), reached));
|
|
193
|
+
const behind = (wanted - motion.position) * direction;
|
|
194
|
+
const reach = Math.max(FILM_LANDING_SPEED * step, behind > 0 ? behind * (1 - Math.exp(-FILM_CATCH_RATE * step)) : 0);
|
|
195
|
+
const position = direction > 0 ? Math.min(goal, motion.position + reach) : Math.max(goal, motion.position - reach);
|
|
196
|
+
return { position, wanted, speed, direction };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Whether a film has landed: still, on the whole picture at or just past where the scroll stands. */
|
|
200
|
+
export function filmLanded(motion: FilmMotion): boolean {
|
|
201
|
+
const past = (motion.position - motion.wanted) * motion.direction;
|
|
202
|
+
return Math.abs(motion.speed) < 0.01 && Number.isInteger(motion.position) && past > -FILM_EPSILON && past < 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
142
205
|
/** The picture at `played` % of a set of `count`. */
|
|
143
206
|
export function frameIndexAt(played: number, count: number): number {
|
|
144
207
|
if (count <= 1) return 0;
|
|
@@ -255,8 +318,9 @@ export interface FramePlayerOptions {
|
|
|
255
318
|
*/
|
|
256
319
|
export class FramePlayer {
|
|
257
320
|
private played: number | null = null;
|
|
258
|
-
private
|
|
259
|
-
private
|
|
321
|
+
private motion: FilmMotion | null = null;
|
|
322
|
+
private steppedAt = 0;
|
|
323
|
+
private landingFrame: number | null = null;
|
|
260
324
|
private readonly set: FrameSet;
|
|
261
325
|
private readonly info: FrameSetInfo;
|
|
262
326
|
private readonly images: Array<HTMLImageElement | null>;
|
|
@@ -265,7 +329,6 @@ export class FramePlayer {
|
|
|
265
329
|
private observer: IntersectionObserver | null = null;
|
|
266
330
|
private canvas: HTMLCanvasElement | null = null;
|
|
267
331
|
private context: CanvasRenderingContext2D | null = null;
|
|
268
|
-
private position = 0;
|
|
269
332
|
private drawn: FrameBlend | null = null;
|
|
270
333
|
private nextToLoad = 0;
|
|
271
334
|
private loading = 0;
|
|
@@ -296,32 +359,26 @@ export class FramePlayer {
|
|
|
296
359
|
seek(played: number): void {
|
|
297
360
|
if (played === this.played) return;
|
|
298
361
|
this.played = played;
|
|
299
|
-
this.
|
|
300
|
-
this.position = framePosition(played, this.info.count);
|
|
301
|
-
this.draw();
|
|
302
|
-
this.restTimer = this.host.setTimeout?.(() => this.rest(), REST_AFTER_MS) ?? null;
|
|
362
|
+
this.step();
|
|
303
363
|
}
|
|
304
364
|
|
|
305
|
-
/**
|
|
306
|
-
private
|
|
307
|
-
this.
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.
|
|
315
|
-
|
|
316
|
-
};
|
|
317
|
-
step();
|
|
365
|
+
/** One step toward the moment asked for, drawn now; a landing goes on by itself until it stands on a whole picture. */
|
|
366
|
+
private step(): void {
|
|
367
|
+
if (this.destroyed || this.played === null) return;
|
|
368
|
+
const wanted = framePosition(this.played, this.info.count);
|
|
369
|
+
const now = this.host.performance?.now() ?? Date.now();
|
|
370
|
+
this.motion = this.motion ? stepFilm(this.motion, wanted, (now - this.steppedAt) / 1000, this.info.count - 1) : filmAt(wanted);
|
|
371
|
+
this.steppedAt = now;
|
|
372
|
+
this.draw();
|
|
373
|
+
if (!filmLanded(this.motion) && this.landingFrame === null) this.landingFrame = this.host.requestAnimationFrame(() => {
|
|
374
|
+
this.landingFrame = null;
|
|
375
|
+
this.step();
|
|
376
|
+
});
|
|
318
377
|
}
|
|
319
378
|
|
|
320
|
-
private
|
|
321
|
-
if (this.
|
|
322
|
-
|
|
323
|
-
this.restTimer = null;
|
|
324
|
-
this.restFrame = null;
|
|
379
|
+
private stopLanding(): void {
|
|
380
|
+
if (this.landingFrame !== null) this.host.cancelAnimationFrame?.(this.landingFrame);
|
|
381
|
+
this.landingFrame = null;
|
|
325
382
|
}
|
|
326
383
|
|
|
327
384
|
/** Whether a re-render took the canvas away with the markup it sat in. */
|
|
@@ -331,7 +388,7 @@ export class FramePlayer {
|
|
|
331
388
|
|
|
332
389
|
destroy(): void {
|
|
333
390
|
this.destroyed = true;
|
|
334
|
-
this.
|
|
391
|
+
this.stopLanding();
|
|
335
392
|
this.el.removeAttribute(FRAMES_SHOWING_ATTRIBUTE);
|
|
336
393
|
this.observer?.disconnect();
|
|
337
394
|
this.observer = null;
|
|
@@ -392,7 +449,8 @@ export class FramePlayer {
|
|
|
392
449
|
}
|
|
393
450
|
|
|
394
451
|
private draw(): void {
|
|
395
|
-
|
|
452
|
+
if (!this.motion) return;
|
|
453
|
+
const blend = frameBlendAt(this.images, this.motion.position);
|
|
396
454
|
const last = this.drawn;
|
|
397
455
|
if (!blend || (last && last.from === blend.from && last.to === blend.to && last.mix === blend.mix)) return;
|
|
398
456
|
const image = this.images[blend.from]!;
|