@vectoriox/iox-ui 5.73.3 → 5.74.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 +51 -12
- package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +14 -10
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/media-core/index.d.ts +26 -3
- package/package.json +1 -1
- package/scene-core/index.d.ts +6 -3
- package/src/media-core/frames.ts +58 -10
- package/src/media-core/video.ts +8 -2
- package/src/scene-core/scene.ts +15 -10
- package/styles/iox-design.css +7 -2
package/media-core/index.d.ts
CHANGED
|
@@ -24,9 +24,16 @@ interface FrameSequence {
|
|
|
24
24
|
wide: FrameSetInfo;
|
|
25
25
|
/** Absent = phones play the wide set. */
|
|
26
26
|
phone?: FrameSetInfo;
|
|
27
|
+
/**
|
|
28
|
+
* A phone film of its own, framed for a tall screen, lives under its own name: a replacement that fails half way leaves
|
|
29
|
+
* the one playing untouched. Absent = the phone set is the wide film made smaller, under `base`.
|
|
30
|
+
*/
|
|
31
|
+
phoneBase?: string;
|
|
27
32
|
/** What the sequence weighs in storage, both sets. Shown to the author. */
|
|
28
33
|
bytes?: number;
|
|
29
34
|
}
|
|
35
|
+
/** At or under this width a screen is a phone. Scene's own limit says the same: scene-core reads media-core, never the reverse. */
|
|
36
|
+
declare const FRAMES_PHONE_MAX_WIDTH = 767;
|
|
30
37
|
declare enum FrameExtension {
|
|
31
38
|
Webp = "webp",
|
|
32
39
|
Jpeg = "jpg"
|
|
@@ -50,6 +57,22 @@ declare function frameSetFor(sequence: FrameSequence, phone: boolean): {
|
|
|
50
57
|
set: FrameSet;
|
|
51
58
|
info: FrameSetInfo;
|
|
52
59
|
};
|
|
60
|
+
/** Where `played` % stands in a set of `count`: 2.5 is half way from the third picture to the fourth. */
|
|
61
|
+
declare function framePosition(played: number, count: number): number;
|
|
62
|
+
/** How finely two neighbours mix. Finer than the eye reads, and a scroll that moves less than a step draws nothing. */
|
|
63
|
+
declare const FRAME_BLEND_STEPS = 32;
|
|
64
|
+
/** What to draw: picture `from`, with `to` over it at `mix` when the moment falls between two that have arrived. */
|
|
65
|
+
interface FrameBlend {
|
|
66
|
+
from: number;
|
|
67
|
+
to: number;
|
|
68
|
+
mix: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The moment between two pictures. A long Scene gives a picture 50px of scroll or more, so a slow scroll would otherwise
|
|
72
|
+
* show the film stepping. Blending the two neighbours keeps it moving under the finger, at the cost of one more drawImage.
|
|
73
|
+
* A neighbour still loading never blends: the nearest arrived picture stands in, and null means none has.
|
|
74
|
+
*/
|
|
75
|
+
declare function frameBlendAt(loaded: ReadonlyArray<unknown>, position: number): FrameBlend | null;
|
|
53
76
|
/** The picture at `played` % of a set of `count`. */
|
|
54
77
|
declare function frameIndexAt(played: number, count: number): number;
|
|
55
78
|
/**
|
|
@@ -112,7 +135,7 @@ declare class FramePlayer {
|
|
|
112
135
|
private observer;
|
|
113
136
|
private canvas;
|
|
114
137
|
private context;
|
|
115
|
-
private
|
|
138
|
+
private position;
|
|
116
139
|
private drawn;
|
|
117
140
|
private nextToLoad;
|
|
118
141
|
private loading;
|
|
@@ -414,5 +437,5 @@ declare function attachVideos(container: ParentNode, options?: VideoRuntimeOptio
|
|
|
414
437
|
/** The runtime already running at `el`, if any. */
|
|
415
438
|
declare function videoRuntimeOf(el: Element | null | undefined): VideoRuntime | null;
|
|
416
439
|
|
|
417
|
-
export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, 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, frameFileName, frameIndexAt, frameLoadOrder, 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 };
|
|
418
|
-
export type { ConsentGrantDetail, ConsentTransport, FramePlayerOptions, FrameSequence, FrameSetInfo, FrameSizes, VideoMarkupOptions, VideoNodeLike, VideoPlan, VideoRef, VideoRuntimeOptions, VideoThumbnailRequest, VideoWords };
|
|
440
|
+
export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FRAMES_PHONE_MAX_WIDTH, 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 };
|
|
441
|
+
export type { ConsentGrantDetail, ConsentTransport, FrameBlend, FramePlayerOptions, FrameSequence, FrameSetInfo, FrameSizes, VideoMarkupOptions, VideoNodeLike, VideoPlan, VideoRef, VideoRuntimeOptions, VideoThumbnailRequest, VideoWords };
|
package/package.json
CHANGED
package/scene-core/index.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ declare function normalizeTracks(tracks: unknown): SceneTrack[];
|
|
|
85
85
|
*/
|
|
86
86
|
declare function revealClipPath(showing: number, from?: SceneRevealFrom, inlineSign?: 1 | -1): string;
|
|
87
87
|
/** A numeric track's value at progress `p`. */
|
|
88
|
-
declare function valueAt(keys: ReadonlyArray<SceneKey>, p: number): number;
|
|
88
|
+
declare function valueAt(keys: ReadonlyArray<SceneKey>, p: number, ease?: (t: number) => number): number;
|
|
89
89
|
/**
|
|
90
90
|
* A colour track's value at progress `p`. Mixed by the browser, never computed here, so a design token stays a live
|
|
91
91
|
* reference (a re-theme reaches a Scene, as it reaches every saved page) and the path is perceptual.
|
|
@@ -104,7 +104,10 @@ declare function sceneFrame(tracks: ReadonlyArray<SceneTrack>, p: number, option
|
|
|
104
104
|
declare const SCENE_WORD_DIM = 0.16;
|
|
105
105
|
/** How many of a layer's words are lit at `p`, in %; null when it has no such track. */
|
|
106
106
|
declare function sceneWordsLit(tracks: ReadonlyArray<SceneTrack>, p: number): number | null;
|
|
107
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* How far a layer's pictures have played at `p`, in %; null when it has no such track. A film runs straight between its
|
|
109
|
+
* keys, where everything else eases: eased over a 20-screen Scene it would crawl for its first screens and rush the middle.
|
|
110
|
+
*/
|
|
108
111
|
declare function scenePlayed(tracks: ReadonlyArray<SceneTrack>, p: number): number | null;
|
|
109
112
|
/** Word `index` of `count`, with `lit` % lit: words light one after the other, each easing in as the last finishes. */
|
|
110
113
|
declare function sceneWordOpacity(index: number, count: number, lit: number): number;
|
|
@@ -130,7 +133,7 @@ declare function applySceneFirstFrame(el: HTMLElement | null | undefined, node:
|
|
|
130
133
|
*/
|
|
131
134
|
declare const SCENE_FLOW_RULES: string;
|
|
132
135
|
/** Under reduced motion the driver never starts, so the same rules hold for the whole visit. */
|
|
133
|
-
declare const SCENE_REDUCED_MOTION_CSS = "@media (prefers-reduced-motion: reduce){.iox-role-scene{height:auto!important}.iox-role-scene-stage{position:relative!important;top:auto!important;height:auto!important;overflow:visible!important;container-type:normal!important}[data-iox-tracks]{transform:none!important;opacity:1!important;clip-path:none!important;pointer-events:auto!important}[data-iox-video-frames]{height:auto!important}[data-iox-video-frames]>.iox-video__poster{position:static!important;height:auto!important}}";
|
|
136
|
+
declare const SCENE_REDUCED_MOTION_CSS = "@media (prefers-reduced-motion: reduce){.iox-role-scene{height:auto!important}.iox-role-scene-stage{position:relative!important;top:auto!important;height:auto!important;overflow:visible!important;container-type:normal!important}[data-iox-tracks]{transform:none!important;opacity:1!important;clip-path:none!important;pointer-events:auto!important}[data-iox-video-frames]{height:auto!important}[data-iox-video-frames]>.iox-video__poster,[data-iox-video-frames]>picture>.iox-video__poster{position:static!important;height:auto!important}}";
|
|
134
137
|
|
|
135
138
|
declare function sceneKeyPosition(p: number): number;
|
|
136
139
|
interface SceneTrackDef {
|
package/src/media-core/frames.ts
CHANGED
|
@@ -25,10 +25,18 @@ export interface FrameSequence {
|
|
|
25
25
|
wide: FrameSetInfo;
|
|
26
26
|
/** Absent = phones play the wide set. */
|
|
27
27
|
phone?: FrameSetInfo;
|
|
28
|
+
/**
|
|
29
|
+
* A phone film of its own, framed for a tall screen, lives under its own name: a replacement that fails half way leaves
|
|
30
|
+
* the one playing untouched. Absent = the phone set is the wide film made smaller, under `base`.
|
|
31
|
+
*/
|
|
32
|
+
phoneBase?: string;
|
|
28
33
|
/** What the sequence weighs in storage, both sets. Shown to the author. */
|
|
29
34
|
bytes?: number;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/** At or under this width a screen is a phone. Scene's own limit says the same: scene-core reads media-core, never the reverse. */
|
|
38
|
+
export const FRAMES_PHONE_MAX_WIDTH = 767;
|
|
39
|
+
|
|
32
40
|
export enum FrameExtension {
|
|
33
41
|
Webp = 'webp',
|
|
34
42
|
Jpeg = 'jpg',
|
|
@@ -58,11 +66,13 @@ export function normalizeFrameSequence(raw: unknown): FrameSequence | null {
|
|
|
58
66
|
const wide = setInfo(value.wide);
|
|
59
67
|
if (!wide || typeof value.base !== 'string' || !SAFE_BASE.test(value.base) || !ALL_EXTENSIONS.has(String(value.ext))) return null;
|
|
60
68
|
const phone = setInfo(value.phone);
|
|
69
|
+
const phoneBase = phone && typeof value.phoneBase === 'string' && SAFE_BASE.test(value.phoneBase) ? value.phoneBase.replace(/\/+$/, '') : '';
|
|
61
70
|
return {
|
|
62
71
|
base: value.base.replace(/\/+$/, ''),
|
|
63
72
|
ext: value.ext as FrameExtension,
|
|
64
73
|
wide,
|
|
65
74
|
...(phone ? { phone } : {}),
|
|
75
|
+
...(phoneBase ? { phoneBase } : {}),
|
|
66
76
|
...(Number.isFinite(value.bytes) && (value.bytes as number) > 0 ? { bytes: value.bytes } : {}),
|
|
67
77
|
};
|
|
68
78
|
}
|
|
@@ -78,7 +88,8 @@ export function frameFileName(index: number, ext: FrameExtension): string {
|
|
|
78
88
|
|
|
79
89
|
/** Picture `index` (from 0) of a set. */
|
|
80
90
|
export function frameUrl(sequence: FrameSequence, set: FrameSet, index: number): string {
|
|
81
|
-
|
|
91
|
+
const base = set === FrameSet.Phone && sequence.phoneBase ? sequence.phoneBase : sequence.base;
|
|
92
|
+
return `${base}/${set}/${frameFileName(index, sequence.ext)}`;
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
/** The first picture of each set, for a poster's `srcset`; '' when there is only one set. */
|
|
@@ -92,6 +103,35 @@ export function frameSetFor(sequence: FrameSequence, phone: boolean): { set: Fra
|
|
|
92
103
|
return phone && sequence.phone ? { set: FrameSet.Phone, info: sequence.phone } : { set: FrameSet.Wide, info: sequence.wide };
|
|
93
104
|
}
|
|
94
105
|
|
|
106
|
+
/** Where `played` % stands in a set of `count`: 2.5 is half way from the third picture to the fourth. */
|
|
107
|
+
export function framePosition(played: number, count: number): number {
|
|
108
|
+
return (Math.min(100, Math.max(0, played)) / 100) * (count - 1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** How finely two neighbours mix. Finer than the eye reads, and a scroll that moves less than a step draws nothing. */
|
|
112
|
+
export const FRAME_BLEND_STEPS = 32;
|
|
113
|
+
|
|
114
|
+
/** What to draw: picture `from`, with `to` over it at `mix` when the moment falls between two that have arrived. */
|
|
115
|
+
export interface FrameBlend {
|
|
116
|
+
from: number;
|
|
117
|
+
to: number;
|
|
118
|
+
mix: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The moment between two pictures. A long Scene gives a picture 50px of scroll or more, so a slow scroll would otherwise
|
|
123
|
+
* show the film stepping. Blending the two neighbours keeps it moving under the finger, at the cost of one more drawImage.
|
|
124
|
+
* A neighbour still loading never blends: the nearest arrived picture stands in, and null means none has.
|
|
125
|
+
*/
|
|
126
|
+
export function frameBlendAt(loaded: ReadonlyArray<unknown>, position: number): FrameBlend | null {
|
|
127
|
+
const from = Math.floor(position);
|
|
128
|
+
const to = from + 1;
|
|
129
|
+
const mix = Math.round((position - from) * FRAME_BLEND_STEPS) / FRAME_BLEND_STEPS;
|
|
130
|
+
if (mix > 0 && mix < 1 && loaded[from] && loaded[to]) return { from, to, mix };
|
|
131
|
+
const nearest = nearestLoadedFrame(loaded, Math.round(position));
|
|
132
|
+
return nearest < 0 ? null : { from: nearest, to: nearest, mix: 0 };
|
|
133
|
+
}
|
|
134
|
+
|
|
95
135
|
/** The picture at `played` % of a set of `count`. */
|
|
96
136
|
export function frameIndexAt(played: number, count: number): number {
|
|
97
137
|
if (count <= 1) return 0;
|
|
@@ -215,8 +255,8 @@ export class FramePlayer {
|
|
|
215
255
|
private observer: IntersectionObserver | null = null;
|
|
216
256
|
private canvas: HTMLCanvasElement | null = null;
|
|
217
257
|
private context: CanvasRenderingContext2D | null = null;
|
|
218
|
-
private
|
|
219
|
-
private drawn =
|
|
258
|
+
private position = 0;
|
|
259
|
+
private drawn: FrameBlend | null = null;
|
|
220
260
|
private nextToLoad = 0;
|
|
221
261
|
private loading = 0;
|
|
222
262
|
private started = false;
|
|
@@ -244,7 +284,7 @@ export class FramePlayer {
|
|
|
244
284
|
|
|
245
285
|
/** Stand at `played` %. */
|
|
246
286
|
seek(played: number): void {
|
|
247
|
-
this.
|
|
287
|
+
this.position = framePosition(played, this.info.count);
|
|
248
288
|
this.draw();
|
|
249
289
|
}
|
|
250
290
|
|
|
@@ -314,13 +354,21 @@ export class FramePlayer {
|
|
|
314
354
|
}
|
|
315
355
|
|
|
316
356
|
private draw(): void {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
357
|
+
const blend = frameBlendAt(this.images, this.position);
|
|
358
|
+
const last = this.drawn;
|
|
359
|
+
if (!blend || (last && last.from === blend.from && last.to === blend.to && last.mix === blend.mix)) return;
|
|
360
|
+
const image = this.images[blend.from]!;
|
|
320
361
|
if (!this.ensureCanvas(image)) return;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
362
|
+
const context = this.context!;
|
|
363
|
+
const { width, height } = this.canvas!;
|
|
364
|
+
context.clearRect(0, 0, width, height);
|
|
365
|
+
context.drawImage(image, 0, 0, width, height);
|
|
366
|
+
if (blend.mix > 0) {
|
|
367
|
+
context.globalAlpha = blend.mix;
|
|
368
|
+
context.drawImage(this.images[blend.to]!, 0, 0, width, height);
|
|
369
|
+
context.globalAlpha = 1;
|
|
370
|
+
}
|
|
371
|
+
this.drawn = blend;
|
|
324
372
|
}
|
|
325
373
|
|
|
326
374
|
private ensureCanvas(image: HTMLImageElement): boolean {
|
package/src/media-core/video.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConsentCategory, ConsentDecision, hasConsent } from '@vectoriox/iox-ui/consent-core';
|
|
2
2
|
|
|
3
|
-
import { FrameSequence, FrameSet, frameUrl, normalizeFrameSequence, phoneFrameSrcset } from './frames';
|
|
3
|
+
import { FRAMES_PHONE_MAX_WIDTH, FrameSequence, FrameSet, frameUrl, normalizeFrameSequence, phoneFrameSrcset } from './frames';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* VIDEO (CC3 — complex-components plan §7b, board 5). A LEAF type with two sources:
|
|
@@ -441,7 +441,13 @@ export function videoInnerHtml(plan: VideoPlan, options: VideoMarkupOptions): st
|
|
|
441
441
|
const srcset = own ? phoneFrameSrcset(plan.frames!) : '';
|
|
442
442
|
// Its real size, so where a Scene flows (reduced motion, no script) the picture holds its height before it loads.
|
|
443
443
|
const size = own ? ` width="${plan.frames!.wide.width}" height="${plan.frames!.wide.height}"` : '';
|
|
444
|
-
|
|
444
|
+
const image = (attrs: string): string => `<img class="${VideoClass.Poster}" src="${esc(plan.poster)}"${attrs} alt="${esc(plan.title)}" decoding="async">`;
|
|
445
|
+
// A phone film is framed for a tall screen, so it is chosen by the screen, never by the pixels a high-density phone asks for.
|
|
446
|
+
if (own && plan.frames!.phoneBase) {
|
|
447
|
+
const { width, height } = plan.frames!.phone!;
|
|
448
|
+
return `<picture><source media="(max-width: ${FRAMES_PHONE_MAX_WIDTH}px)" srcset="${esc(frameUrl(plan.frames!, FrameSet.Phone, 0))}" width="${width}" height="${height}">${image(size)}</picture>`;
|
|
449
|
+
}
|
|
450
|
+
return image(`${srcset ? ` srcset="${esc(srcset)}" sizes="100vw"` : ''}${size}`);
|
|
445
451
|
}
|
|
446
452
|
const poster = plan.poster ? `<img class="${VideoClass.Poster}" src="${esc(plan.poster)}" alt="" loading="lazy" decoding="async">` : '';
|
|
447
453
|
const label = esc(videoPlayLabel(plan, words));
|
package/src/scene-core/scene.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SceneRole } from '@vectoriox/iox-ui/composite-core';
|
|
2
|
-
import { VideoAttribute, VideoClass } from '@vectoriox/iox-ui/media-core';
|
|
2
|
+
import { FRAMES_PHONE_MAX_WIDTH, VideoAttribute, VideoClass } from '@vectoriox/iox-ui/media-core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A Scene's data and maths (premium-sites plan §5b). A Scene is a recipe of ordinary nodes — a tall Section, a pinned
|
|
@@ -83,7 +83,7 @@ export enum SceneAttribute {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/** At or under this stage width a layer plays its phone tracks. The `mobile` breakpoint's width. */
|
|
86
|
-
export const SCENE_PHONE_MAX_WIDTH =
|
|
86
|
+
export const SCENE_PHONE_MAX_WIDTH = FRAMES_PHONE_MAX_WIDTH;
|
|
87
87
|
|
|
88
88
|
const DECIMALS = 10000;
|
|
89
89
|
const round = (v: number): number => Math.round(v * DECIMALS) / DECIMALS;
|
|
@@ -150,8 +150,10 @@ export function revealClipPath(showing: number, from: SceneRevealFrom = SCENE_RE
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const linear = (t: number): number => t;
|
|
154
|
+
|
|
155
|
+
/** The two keys around `p` and how far between them it has come. */
|
|
156
|
+
function span(keys: ReadonlyArray<SceneKey>, p: number, ease: (t: number) => number = smoothstep): { a: SceneKey; b: SceneKey; f: number } {
|
|
155
157
|
const at = clamp01(p);
|
|
156
158
|
if (at <= keys[0][0]) return { a: keys[0], b: keys[0], f: 0 };
|
|
157
159
|
for (let i = 1; i < keys.length; i++) {
|
|
@@ -159,7 +161,7 @@ function span(keys: ReadonlyArray<SceneKey>, p: number): { a: SceneKey; b: Scene
|
|
|
159
161
|
const a = keys[i - 1];
|
|
160
162
|
const b = keys[i];
|
|
161
163
|
const width = b[0] - a[0];
|
|
162
|
-
return { a, b, f: width > 0 ?
|
|
164
|
+
return { a, b, f: width > 0 ? ease((at - a[0]) / width) : 1 };
|
|
163
165
|
}
|
|
164
166
|
}
|
|
165
167
|
const last = keys[keys.length - 1];
|
|
@@ -167,9 +169,9 @@ function span(keys: ReadonlyArray<SceneKey>, p: number): { a: SceneKey; b: Scene
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
/** A numeric track's value at progress `p`. */
|
|
170
|
-
export function valueAt(keys: ReadonlyArray<SceneKey>, p: number): number {
|
|
172
|
+
export function valueAt(keys: ReadonlyArray<SceneKey>, p: number, ease?: (t: number) => number): number {
|
|
171
173
|
if (!keys.length) return 0;
|
|
172
|
-
const { a, b, f } = span(keys, p);
|
|
174
|
+
const { a, b, f } = span(keys, p, ease);
|
|
173
175
|
const from = Number(a[1]);
|
|
174
176
|
return round(from + (Number(b[1]) - from) * f);
|
|
175
177
|
}
|
|
@@ -229,10 +231,13 @@ export function sceneWordsLit(tracks: ReadonlyArray<SceneTrack>, p: number): num
|
|
|
229
231
|
return track ? clampPercent(valueAt(track.keys, p)) : null;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* How far a layer's pictures have played at `p`, in %; null when it has no such track. A film runs straight between its
|
|
236
|
+
* keys, where everything else eases: eased over a 20-screen Scene it would crawl for its first screens and rush the middle.
|
|
237
|
+
*/
|
|
233
238
|
export function scenePlayed(tracks: ReadonlyArray<SceneTrack>, p: number): number | null {
|
|
234
239
|
const track = tracks.find(t => t.prop === SceneProp.Play);
|
|
235
|
-
return track ? clampPercent(valueAt(track.keys, p)) : null;
|
|
240
|
+
return track ? clampPercent(valueAt(track.keys, p, linear)) : null;
|
|
236
241
|
}
|
|
237
242
|
|
|
238
243
|
/** Word `index` of `count`, with `lit` % lit: words light one after the other, each easing in as the last finishes. */
|
|
@@ -307,7 +312,7 @@ export const SCENE_FLOW_RULES = `.iox-role-scene{height:auto!important}`
|
|
|
307
312
|
+ `[${SceneAttribute.Tracks}]{transform:none!important;opacity:1!important;clip-path:none!important;pointer-events:auto!important}`
|
|
308
313
|
// A Video the scroll plays stands as its poster, at the poster's own height: a percent of a stage that now flows is nothing.
|
|
309
314
|
+ `[${VideoAttribute.Frames}]{height:auto!important}`
|
|
310
|
-
+ `[${VideoAttribute.Frames}]>.${VideoClass.Poster}{position:static!important;height:auto!important}`;
|
|
315
|
+
+ `[${VideoAttribute.Frames}]>.${VideoClass.Poster},[${VideoAttribute.Frames}]>picture>.${VideoClass.Poster}{position:static!important;height:auto!important}`;
|
|
311
316
|
|
|
312
317
|
/** Under reduced motion the driver never starts, so the same rules hold for the whole visit. */
|
|
313
318
|
export const SCENE_REDUCED_MOTION_CSS = `@media (prefers-reduced-motion: reduce){${SCENE_FLOW_RULES}}`;
|
package/styles/iox-design.css
CHANGED
|
@@ -348,12 +348,17 @@ dialog[data-iox-dialog]:not([open]) {
|
|
|
348
348
|
container-type: inline-size;
|
|
349
349
|
}
|
|
350
350
|
/* The poster blank (DS3): no poster yet = the token fill, never a request to the provider. */
|
|
351
|
+
/* A Video the scroll plays, given a height: without its own width, a 16 / 9 ratio would work its width out from that height
|
|
352
|
+
and run off the stage. Zero specificity, so the node's own width wins. */
|
|
353
|
+
:where(.iox-video[data-iox-video-frames]) { width: 100%; }
|
|
351
354
|
.iox-video[data-iox-video-poster-empty]:not([data-iox-video-playing]) { background-color: var(--iox-surface-2); }
|
|
352
355
|
.iox-video[data-iox-video-playing] { background-color: var(--iox-inverse-bg); }
|
|
353
|
-
.iox-video >
|
|
356
|
+
.iox-video > picture { display: contents; }
|
|
357
|
+
.iox-video > :is(.iox-video__poster, .iox-video__media, .iox-video__frame, .iox-video__frames),
|
|
358
|
+
.iox-video > picture > .iox-video__poster {
|
|
354
359
|
position: absolute; inset: 0; width: 100%; height: 100%; border: 0; margin: 0; display: block; object-fit: cover;
|
|
355
360
|
}
|
|
356
|
-
.iox-video > .iox-video__poster { z-index: -1; }
|
|
361
|
+
.iox-video > .iox-video__poster, .iox-video > picture > .iox-video__poster { z-index: -1; }
|
|
357
362
|
.iox-video > .iox-video__media { object-fit: contain; background-color: var(--iox-inverse-bg); }
|
|
358
363
|
.iox-video[data-iox-video-mode='background'] > .iox-video__media { object-fit: cover; }
|
|
359
364
|
.iox-video__play {
|