@vectoriox/iox-ui 5.74.7 → 5.75.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.
- package/fesm2022/vectoriox-iox-ui-media-core.mjs +88 -38
- package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +6 -5
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/media-core/index.d.ts +42 -10
- package/package.json +1 -1
- package/src/media-core/frames.ts +98 -33
- package/src/scene-core/scroll-driver.ts +6 -5
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,28 @@ 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 at or past where the scroll will stop, and stands
|
|
100
|
+
* there: it never crawls through a dissolve, never stops between two pictures, and never steps back unless the scroll
|
|
101
|
+
* does. `aim` is where the scroll is heading, when the driver knows it (its undamped target); without it, it is guessed.
|
|
102
|
+
*/
|
|
103
|
+
declare function stepFilm(motion: FilmMotion, wanted: number, seconds: number, last: number, aim?: number): FilmMotion;
|
|
104
|
+
/** Whether a film has landed: still, on the whole picture at or just past where the scroll stands. */
|
|
105
|
+
declare function filmLanded(motion: FilmMotion): boolean;
|
|
78
106
|
/** The picture at `played` % of a set of `count`. */
|
|
79
107
|
declare function frameIndexAt(played: number, count: number): number;
|
|
80
108
|
/**
|
|
@@ -130,8 +158,10 @@ declare class FramePlayer {
|
|
|
130
158
|
private readonly el;
|
|
131
159
|
private readonly sequence;
|
|
132
160
|
private played;
|
|
133
|
-
private
|
|
134
|
-
private
|
|
161
|
+
private aim;
|
|
162
|
+
private motion;
|
|
163
|
+
private steppedAt;
|
|
164
|
+
private landingFrame;
|
|
135
165
|
private readonly set;
|
|
136
166
|
private readonly info;
|
|
137
167
|
private readonly images;
|
|
@@ -140,7 +170,6 @@ declare class FramePlayer {
|
|
|
140
170
|
private observer;
|
|
141
171
|
private canvas;
|
|
142
172
|
private context;
|
|
143
|
-
private position;
|
|
144
173
|
private drawn;
|
|
145
174
|
private nextToLoad;
|
|
146
175
|
private loading;
|
|
@@ -148,11 +177,14 @@ declare class FramePlayer {
|
|
|
148
177
|
private destroyed;
|
|
149
178
|
private readonly pending;
|
|
150
179
|
constructor(el: HTMLElement, sequence: FrameSequence, options?: FramePlayerOptions);
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Stand at `played` %, on the way to `aim` % when the caller knows where the scroll is going. `now` is the caller's
|
|
182
|
+
* frame time: the player keeps time by frames, so a frame asked twice moves once however long the page took.
|
|
183
|
+
*/
|
|
184
|
+
seek(played: number, aim?: number, now?: number): void;
|
|
185
|
+
/** One step toward the moment asked for, drawn now; a landing goes on by itself until it stands on a whole picture. */
|
|
186
|
+
private step;
|
|
187
|
+
private stopLanding;
|
|
156
188
|
/** Whether a re-render took the canvas away with the markup it sat in. */
|
|
157
189
|
get detached(): boolean;
|
|
158
190
|
destroy(): void;
|
|
@@ -445,5 +477,5 @@ declare function attachVideos(container: ParentNode, options?: VideoRuntimeOptio
|
|
|
445
477
|
/** The runtime already running at `el`, if any. */
|
|
446
478
|
declare function videoRuntimeOf(el: Element | null | undefined): VideoRuntime | null;
|
|
447
479
|
|
|
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 };
|
|
480
|
+
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 };
|
|
481
|
+
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
|
+
/** With no word from the driver on where the scroll is going, how fast a landing scroll is assumed to slow (per second). */
|
|
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,58 @@ 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 at or past where the scroll will stop, and stands
|
|
175
|
+
* there: it never crawls through a dissolve, never stops between two pictures, and never steps back unless the scroll
|
|
176
|
+
* does. `aim` is where the scroll is heading, when the driver knows it (its undamped target); without it, it is guessed.
|
|
177
|
+
*/
|
|
178
|
+
export function stepFilm(motion: FilmMotion, wanted: number, seconds: number, last: number, aim?: number): FilmMotion {
|
|
179
|
+
const sameFrame = seconds < FILM_SAME_FRAME_SECONDS;
|
|
180
|
+
const step = Math.min(FILM_MAX_STEP_SECONDS, seconds);
|
|
181
|
+
const moved = wanted - motion.wanted;
|
|
182
|
+
const speed = sameFrame ? motion.speed : motion.speed + (moved / step - motion.speed) * (1 - Math.exp(-FILM_SPEED_RATE * step));
|
|
183
|
+
const heading = aim === undefined ? moved : aim - wanted;
|
|
184
|
+
const direction: 1 | -1 = Math.abs(heading) > FILM_EPSILON ? (heading > 0 ? 1 : -1) : Math.abs(moved) > FILM_EPSILON ? (moved > 0 ? 1 : -1) : motion.direction;
|
|
185
|
+
// Moving: with the scroll, but never back over a picture a landing already reached unless the scroll turned.
|
|
186
|
+
if (Math.abs(speed) >= FILM_LANDING_SPEED) {
|
|
187
|
+
const position = direction > 0 ? Math.max(wanted, Math.min(motion.position, Math.ceil(wanted))) : Math.min(wanted, Math.max(motion.position, Math.floor(wanted)));
|
|
188
|
+
return { position, wanted, speed, direction };
|
|
189
|
+
}
|
|
190
|
+
if (sameFrame) return { ...motion, wanted, direction };
|
|
191
|
+
const clamp = (value: number): number => Math.min(last, Math.max(0, value));
|
|
192
|
+
const stop = aim ?? wanted + speed / FILM_LANDING_DECAY;
|
|
193
|
+
const reached = direction > 0 ? Math.ceil(motion.position - FILM_EPSILON) : Math.floor(motion.position + FILM_EPSILON);
|
|
194
|
+
const goal = clamp(direction > 0 ? Math.max(Math.ceil(stop - FILM_EPSILON), reached) : Math.min(Math.floor(stop + FILM_EPSILON), reached));
|
|
195
|
+
const behind = (wanted - motion.position) * direction;
|
|
196
|
+
const reach = Math.max(FILM_LANDING_SPEED * step, behind > 0 ? behind * (1 - Math.exp(-FILM_CATCH_RATE * step)) : 0);
|
|
197
|
+
const position = direction > 0 ? Math.min(goal, motion.position + reach) : Math.max(goal, motion.position - reach);
|
|
198
|
+
return { position, wanted, speed, direction };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Whether a film has landed: still, on the whole picture at or just past where the scroll stands. */
|
|
202
|
+
export function filmLanded(motion: FilmMotion): boolean {
|
|
203
|
+
const past = (motion.position - motion.wanted) * motion.direction;
|
|
204
|
+
return Math.abs(motion.speed) < 0.01 && Number.isInteger(motion.position) && past > -FILM_EPSILON && past < 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
142
207
|
/** The picture at `played` % of a set of `count`. */
|
|
143
208
|
export function frameIndexAt(played: number, count: number): number {
|
|
144
209
|
if (count <= 1) return 0;
|
|
@@ -255,8 +320,10 @@ export interface FramePlayerOptions {
|
|
|
255
320
|
*/
|
|
256
321
|
export class FramePlayer {
|
|
257
322
|
private played: number | null = null;
|
|
258
|
-
private
|
|
259
|
-
private
|
|
323
|
+
private aim: number | undefined;
|
|
324
|
+
private motion: FilmMotion | null = null;
|
|
325
|
+
private steppedAt = 0;
|
|
326
|
+
private landingFrame: number | null = null;
|
|
260
327
|
private readonly set: FrameSet;
|
|
261
328
|
private readonly info: FrameSetInfo;
|
|
262
329
|
private readonly images: Array<HTMLImageElement | null>;
|
|
@@ -265,7 +332,6 @@ export class FramePlayer {
|
|
|
265
332
|
private observer: IntersectionObserver | null = null;
|
|
266
333
|
private canvas: HTMLCanvasElement | null = null;
|
|
267
334
|
private context: CanvasRenderingContext2D | null = null;
|
|
268
|
-
private position = 0;
|
|
269
335
|
private drawn: FrameBlend | null = null;
|
|
270
336
|
private nextToLoad = 0;
|
|
271
337
|
private loading = 0;
|
|
@@ -292,36 +358,34 @@ export class FramePlayer {
|
|
|
292
358
|
this.observer.observe(options.near ?? el);
|
|
293
359
|
}
|
|
294
360
|
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Stand at `played` %, on the way to `aim` % when the caller knows where the scroll is going. `now` is the caller's
|
|
363
|
+
* frame time: the player keeps time by frames, so a frame asked twice moves once however long the page took.
|
|
364
|
+
*/
|
|
365
|
+
seek(played: number, aim?: number, now?: number): void {
|
|
366
|
+
if (played === this.played && aim === this.aim) return;
|
|
298
367
|
this.played = played;
|
|
299
|
-
this.
|
|
300
|
-
this.
|
|
301
|
-
this.draw();
|
|
302
|
-
this.restTimer = this.host.setTimeout?.(() => this.rest(), REST_AFTER_MS) ?? null;
|
|
368
|
+
this.aim = aim;
|
|
369
|
+
this.step(now ?? this.host.performance?.now() ?? 0);
|
|
303
370
|
}
|
|
304
371
|
|
|
305
|
-
/**
|
|
306
|
-
private
|
|
307
|
-
this.
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.
|
|
315
|
-
|
|
316
|
-
};
|
|
317
|
-
step();
|
|
372
|
+
/** One step toward the moment asked for, drawn now; a landing goes on by itself until it stands on a whole picture. */
|
|
373
|
+
private step(now: number): void {
|
|
374
|
+
if (this.destroyed || this.played === null) return;
|
|
375
|
+
const wanted = framePosition(this.played, this.info.count);
|
|
376
|
+
const aim = this.aim === undefined ? undefined : framePosition(this.aim, this.info.count);
|
|
377
|
+
this.motion = this.motion ? stepFilm(this.motion, wanted, (now - this.steppedAt) / 1000, this.info.count - 1, aim) : filmAt(wanted);
|
|
378
|
+
this.steppedAt = now;
|
|
379
|
+
this.draw();
|
|
380
|
+
if (!filmLanded(this.motion) && this.landingFrame === null) this.landingFrame = this.host.requestAnimationFrame(time => {
|
|
381
|
+
this.landingFrame = null;
|
|
382
|
+
this.step(time);
|
|
383
|
+
});
|
|
318
384
|
}
|
|
319
385
|
|
|
320
|
-
private
|
|
321
|
-
if (this.
|
|
322
|
-
|
|
323
|
-
this.restTimer = null;
|
|
324
|
-
this.restFrame = null;
|
|
386
|
+
private stopLanding(): void {
|
|
387
|
+
if (this.landingFrame !== null) this.host.cancelAnimationFrame?.(this.landingFrame);
|
|
388
|
+
this.landingFrame = null;
|
|
325
389
|
}
|
|
326
390
|
|
|
327
391
|
/** Whether a re-render took the canvas away with the markup it sat in. */
|
|
@@ -331,7 +395,7 @@ export class FramePlayer {
|
|
|
331
395
|
|
|
332
396
|
destroy(): void {
|
|
333
397
|
this.destroyed = true;
|
|
334
|
-
this.
|
|
398
|
+
this.stopLanding();
|
|
335
399
|
this.el.removeAttribute(FRAMES_SHOWING_ATTRIBUTE);
|
|
336
400
|
this.observer?.disconnect();
|
|
337
401
|
this.observer = null;
|
|
@@ -392,7 +456,8 @@ export class FramePlayer {
|
|
|
392
456
|
}
|
|
393
457
|
|
|
394
458
|
private draw(): void {
|
|
395
|
-
|
|
459
|
+
if (!this.motion) return;
|
|
460
|
+
const blend = frameBlendAt(this.images, this.motion.position);
|
|
396
461
|
const last = this.drawn;
|
|
397
462
|
if (!blend || (last && last.from === blend.from && last.to === blend.to && last.mix === blend.mix)) return;
|
|
398
463
|
const image = this.images[blend.from]!;
|
|
@@ -267,7 +267,7 @@ export class ScrollDriver {
|
|
|
267
267
|
scene.progress = settled ? target : next;
|
|
268
268
|
scene.target = target;
|
|
269
269
|
scene.fresh = false;
|
|
270
|
-
this.write(scene);
|
|
270
|
+
this.write(scene, now);
|
|
271
271
|
this.options.onFrame?.(scene.root, { target, progress: scene.progress });
|
|
272
272
|
moving = moving || !settled;
|
|
273
273
|
}
|
|
@@ -283,11 +283,11 @@ export class ScrollDriver {
|
|
|
283
283
|
return { scene, local, target: progress };
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
private write(scene: Scene): void {
|
|
286
|
+
private write(scene: Scene, now: number): void {
|
|
287
287
|
for (const layer of scene.layers) {
|
|
288
288
|
const tracks = scene.phone && layer.phoneTracks.length ? layer.phoneTracks : layer.tracks;
|
|
289
289
|
this.writeWords(layer, sceneWordsLit(tracks, scene.progress));
|
|
290
|
-
this.writePlayed(layer, scene, scenePlayed(tracks, scene.progress));
|
|
290
|
+
this.writePlayed(layer, scene, scenePlayed(tracks, scene.progress), scenePlayed(tracks, scene.target), now);
|
|
291
291
|
const frame = sceneFrame(tracks, scene.progress, { inlineSign: scene.inlineSign });
|
|
292
292
|
for (const key of Object.keys(frame) as (keyof SceneFrame)[]) {
|
|
293
293
|
if (layer.stale || frame[key] !== layer.last[key]) layer.el.style[key] = frame[key] ?? '';
|
|
@@ -305,7 +305,7 @@ export class ScrollDriver {
|
|
|
305
305
|
* The picture this moment asks for. The attribute sits on the Video's own root, which is the layer on the string path
|
|
306
306
|
* and inside it on the component path. A layer with no pictures, or no Play track, holds its poster.
|
|
307
307
|
*/
|
|
308
|
-
private writePlayed(layer: Layer, scene: Scene, played: number | null): void {
|
|
308
|
+
private writePlayed(layer: Layer, scene: Scene, played: number | null, aim: number | null, now: number): void {
|
|
309
309
|
const target = played === null ? null : (layer.el.hasAttribute(VideoAttribute.Frames) ? layer.el : layer.el.querySelector<HTMLElement>(`[${VideoAttribute.Frames}]`));
|
|
310
310
|
const raw = target?.getAttribute(VideoAttribute.Frames) ?? '';
|
|
311
311
|
const sequence = raw ? normalizeFrameSequence(raw) : null;
|
|
@@ -319,7 +319,8 @@ export class ScrollDriver {
|
|
|
319
319
|
layer.player = new FramePlayer(target, sequence, { phone: scene.phone, near: scene.root, host: this.host });
|
|
320
320
|
layer.playerKey = key;
|
|
321
321
|
}
|
|
322
|
-
|
|
322
|
+
// Where the damping is heading, so a landing film stops on the picture the scroll stops on, not one past it.
|
|
323
|
+
layer.player.seek(played!, aim ?? undefined, now);
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
/** One opacity per word, and a write only for the few the scroll actually changed. */
|