@vectoriox/iox-ui 5.75.0 → 5.75.3
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 +27 -20
- 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 +11 -6
- package/package.json +1 -1
- package/src/media-core/frames.ts +28 -20
- 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
|
|
175
|
-
* never crawls through a dissolve, never stops between two pictures, and never steps back
|
|
174
|
+
* pictures a second) it runs on at that speed to the whole picture nearest where the scroll will stop, and stands there:
|
|
175
|
+
* it never crawls through a dissolve, never stops between two pictures, and never steps back over a picture it reached
|
|
176
|
+
* unless the scroll turns. `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,19 +189,20 @@ 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;
|
|
191
|
-
|
|
192
|
-
const
|
|
192
|
+
const stop = aim ?? wanted + speed / FILM_LANDING_DECAY;
|
|
193
|
+
// The picture nearest the stop, unless reaching it would show a step back: then the next whole one on the way.
|
|
194
|
+
const nearest = Math.round(stop);
|
|
195
|
+
const back = (motion.position - nearest) * direction > 1 / FRAME_BLEND_STEPS;
|
|
196
|
+
const goal = clamp(back ? (direction > 0 ? Math.ceil(motion.position) : Math.floor(motion.position)) : nearest);
|
|
193
197
|
const behind = (wanted - motion.position) * direction;
|
|
194
198
|
const reach = Math.max(FILM_LANDING_SPEED * step, behind > 0 ? behind * (1 - Math.exp(-FILM_CATCH_RATE * step)) : 0);
|
|
195
199
|
const position = direction > 0 ? Math.min(goal, motion.position + reach) : Math.max(goal, motion.position - reach);
|
|
196
200
|
return { position, wanted, speed, direction };
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
/** Whether a film has landed: still, on
|
|
203
|
+
/** Whether a film has landed: still, on a whole picture beside where the scroll stands. */
|
|
200
204
|
export function filmLanded(motion: FilmMotion): boolean {
|
|
201
|
-
|
|
202
|
-
return Math.abs(motion.speed) < 0.01 && Number.isInteger(motion.position) && past > -FILM_EPSILON && past < 1;
|
|
205
|
+
return Math.abs(motion.speed) < 0.01 && Number.isInteger(motion.position) && Math.abs(motion.position - motion.wanted) < 1;
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
/** The picture at `played` % of a set of `count`. */
|
|
@@ -318,6 +321,7 @@ export interface FramePlayerOptions {
|
|
|
318
321
|
*/
|
|
319
322
|
export class FramePlayer {
|
|
320
323
|
private played: number | null = null;
|
|
324
|
+
private aim: number | undefined;
|
|
321
325
|
private motion: FilmMotion | null = null;
|
|
322
326
|
private steppedAt = 0;
|
|
323
327
|
private landingFrame: number | null = null;
|
|
@@ -355,24 +359,28 @@ export class FramePlayer {
|
|
|
355
359
|
this.observer.observe(options.near ?? el);
|
|
356
360
|
}
|
|
357
361
|
|
|
358
|
-
/**
|
|
359
|
-
|
|
360
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Stand at `played` %, on the way to `aim` % when the caller knows where the scroll is going. `now` is the caller's
|
|
364
|
+
* frame time: the player keeps time by frames, so a frame asked twice moves once however long the page took.
|
|
365
|
+
*/
|
|
366
|
+
seek(played: number, aim?: number, now?: number): void {
|
|
367
|
+
if (played === this.played && aim === this.aim) return;
|
|
361
368
|
this.played = played;
|
|
362
|
-
this.
|
|
369
|
+
this.aim = aim;
|
|
370
|
+
this.step(now ?? this.host.performance?.now() ?? 0);
|
|
363
371
|
}
|
|
364
372
|
|
|
365
373
|
/** 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 {
|
|
374
|
+
private step(now: number): void {
|
|
367
375
|
if (this.destroyed || this.played === null) return;
|
|
368
376
|
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);
|
|
377
|
+
const aim = this.aim === undefined ? undefined : framePosition(this.aim, this.info.count);
|
|
378
|
+
this.motion = this.motion ? stepFilm(this.motion, wanted, (now - this.steppedAt) / 1000, this.info.count - 1, aim) : filmAt(wanted);
|
|
371
379
|
this.steppedAt = now;
|
|
372
380
|
this.draw();
|
|
373
|
-
if (!filmLanded(this.motion) && this.landingFrame === null) this.landingFrame = this.host.requestAnimationFrame(
|
|
381
|
+
if (!filmLanded(this.motion) && this.landingFrame === null) this.landingFrame = this.host.requestAnimationFrame(time => {
|
|
374
382
|
this.landingFrame = null;
|
|
375
|
-
this.step();
|
|
383
|
+
this.step(time);
|
|
376
384
|
});
|
|
377
385
|
}
|
|
378
386
|
|
|
@@ -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. */
|