@vectoriox/iox-ui 5.75.0 → 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 +21 -15
- 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 +10 -5
- package/package.json +1 -1
- package/src/media-core/frames.ts +22 -15
- package/src/scene-core/scroll-driver.ts +6 -5
package/src/media-core/frames.ts
CHANGED
|
@@ -117,7 +117,7 @@ export const FRAMES_SHOWING_ATTRIBUTE = 'data-iox-frames-showing';
|
|
|
117
117
|
* the subject showing twice, and then settling. Landing at least this fast keeps the end of the move a move.
|
|
118
118
|
*/
|
|
119
119
|
export const FILM_LANDING_SPEED = 10;
|
|
120
|
-
/**
|
|
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
121
|
const FILM_LANDING_DECAY = 5;
|
|
122
122
|
/** How quickly the film catches up with a scroll that turned back while it was landing ahead of it (per second). */
|
|
123
123
|
const FILM_CATCH_RATE = 25;
|
|
@@ -171,15 +171,17 @@ export function filmAt(wanted: number): FilmMotion {
|
|
|
171
171
|
|
|
172
172
|
/**
|
|
173
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
|
|
175
|
-
* never crawls through a dissolve, never stops between two pictures, and never steps back unless the scroll
|
|
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.
|
|
176
177
|
*/
|
|
177
|
-
export function stepFilm(motion: FilmMotion, wanted: number, seconds: number, last: number): FilmMotion {
|
|
178
|
+
export function stepFilm(motion: FilmMotion, wanted: number, seconds: number, last: number, aim?: number): FilmMotion {
|
|
178
179
|
const sameFrame = seconds < FILM_SAME_FRAME_SECONDS;
|
|
179
180
|
const step = Math.min(FILM_MAX_STEP_SECONDS, seconds);
|
|
180
181
|
const moved = wanted - motion.wanted;
|
|
181
182
|
const speed = sameFrame ? motion.speed : motion.speed + (moved / step - motion.speed) * (1 - Math.exp(-FILM_SPEED_RATE * step));
|
|
182
|
-
const
|
|
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;
|
|
183
185
|
// Moving: with the scroll, but never back over a picture a landing already reached unless the scroll turned.
|
|
184
186
|
if (Math.abs(speed) >= FILM_LANDING_SPEED) {
|
|
185
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)));
|
|
@@ -187,7 +189,7 @@ export function stepFilm(motion: FilmMotion, wanted: number, seconds: number, la
|
|
|
187
189
|
}
|
|
188
190
|
if (sameFrame) return { ...motion, wanted, direction };
|
|
189
191
|
const clamp = (value: number): number => Math.min(last, Math.max(0, value));
|
|
190
|
-
const stop = wanted + speed / FILM_LANDING_DECAY;
|
|
192
|
+
const stop = aim ?? wanted + speed / FILM_LANDING_DECAY;
|
|
191
193
|
const reached = direction > 0 ? Math.ceil(motion.position - FILM_EPSILON) : Math.floor(motion.position + FILM_EPSILON);
|
|
192
194
|
const goal = clamp(direction > 0 ? Math.max(Math.ceil(stop - FILM_EPSILON), reached) : Math.min(Math.floor(stop + FILM_EPSILON), reached));
|
|
193
195
|
const behind = (wanted - motion.position) * direction;
|
|
@@ -318,6 +320,7 @@ export interface FramePlayerOptions {
|
|
|
318
320
|
*/
|
|
319
321
|
export class FramePlayer {
|
|
320
322
|
private played: number | null = null;
|
|
323
|
+
private aim: number | undefined;
|
|
321
324
|
private motion: FilmMotion | null = null;
|
|
322
325
|
private steppedAt = 0;
|
|
323
326
|
private landingFrame: number | null = null;
|
|
@@ -355,24 +358,28 @@ export class FramePlayer {
|
|
|
355
358
|
this.observer.observe(options.near ?? el);
|
|
356
359
|
}
|
|
357
360
|
|
|
358
|
-
/**
|
|
359
|
-
|
|
360
|
-
|
|
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;
|
|
361
367
|
this.played = played;
|
|
362
|
-
this.
|
|
368
|
+
this.aim = aim;
|
|
369
|
+
this.step(now ?? this.host.performance?.now() ?? 0);
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
/** 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 {
|
|
373
|
+
private step(now: number): void {
|
|
367
374
|
if (this.destroyed || this.played === null) return;
|
|
368
375
|
const wanted = framePosition(this.played, this.info.count);
|
|
369
|
-
const
|
|
370
|
-
this.motion = this.motion ? stepFilm(this.motion, wanted, (now - this.steppedAt) / 1000, this.info.count - 1) : filmAt(wanted);
|
|
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);
|
|
371
378
|
this.steppedAt = now;
|
|
372
379
|
this.draw();
|
|
373
|
-
if (!filmLanded(this.motion) && this.landingFrame === null) this.landingFrame = this.host.requestAnimationFrame(
|
|
380
|
+
if (!filmLanded(this.motion) && this.landingFrame === null) this.landingFrame = this.host.requestAnimationFrame(time => {
|
|
374
381
|
this.landingFrame = null;
|
|
375
|
-
this.step();
|
|
382
|
+
this.step(time);
|
|
376
383
|
});
|
|
377
384
|
}
|
|
378
385
|
|
|
@@ -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. */
|