@vectoriox/iox-ui 5.72.1 → 5.73.1
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-generation-core.mjs +2 -0
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-media-core.mjs +341 -1
- package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +59 -4
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +7 -2
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +3 -1
- package/media-core/index.d.ts +146 -5
- package/package.json +1 -1
- package/scene-core/index.d.ts +19 -2
- package/src/generation-core/node-baselines.generated.ts +2 -0
- package/src/media-core/frames.ts +339 -0
- package/src/media-core/public-api.ts +1 -0
- package/src/media-core/video.ts +51 -0
- package/src/scene-core/scene-edit.ts +15 -0
- package/src/scene-core/scene.ts +13 -1
- package/src/scene-core/scroll-driver.ts +37 -3
- package/styles/iox-design.css +1 -1
|
@@ -1,5 +1,298 @@
|
|
|
1
1
|
import { ConsentCategory, hasConsent, CONSENT_COOKIE_NAME, decode, setCategoryDecision, encode, cookieAttributes } from '@vectoriox/iox-ui/consent-core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A video played by the scroll, as pictures (premium sites SC4). Every source, a video file or a folder of rendered frames,
|
|
5
|
+
* becomes the same numbered set of pictures, prepared once in the author's browser, one set for wide screens and a lighter
|
|
6
|
+
* one for phones. So there is one player and it is the same on every browser: a `<video>` seeked by the scroll needs a
|
|
7
|
+
* dense-keyframe encode no pod here can run, and on an iPhone it accepts seeks it never answers.
|
|
8
|
+
*
|
|
9
|
+
* The saved value is a small description, never a list of addresses: a page holding 300 URLs would ship them in its HTML.
|
|
10
|
+
*/
|
|
11
|
+
var FrameSet;
|
|
12
|
+
(function (FrameSet) {
|
|
13
|
+
FrameSet["Wide"] = "w";
|
|
14
|
+
FrameSet["Phone"] = "p";
|
|
15
|
+
})(FrameSet || (FrameSet = {}));
|
|
16
|
+
var FrameExtension;
|
|
17
|
+
(function (FrameExtension) {
|
|
18
|
+
FrameExtension["Webp"] = "webp";
|
|
19
|
+
FrameExtension["Jpeg"] = "jpg";
|
|
20
|
+
})(FrameExtension || (FrameExtension = {}));
|
|
21
|
+
/** The most pictures one set may hold. Past this a phone runs out of memory before the visitor runs out of scroll. */
|
|
22
|
+
const MAX_FRAMES = 300;
|
|
23
|
+
/** The most one picture may weigh when uploaded. */
|
|
24
|
+
const MAX_FRAME_BYTES = 3 * 1024 * 1024;
|
|
25
|
+
/** A sequence's name in storage: what the browser generates and what the server accepts in a key. */
|
|
26
|
+
const FRAME_SEQUENCE_ID = /^[a-z0-9]{8,40}$/;
|
|
27
|
+
const SAFE_BASE = /^https:\/\/[^\s"'<>\\]+$/;
|
|
28
|
+
const PAD = 4;
|
|
29
|
+
const ALL_EXTENSIONS = new Set(Object.values(FrameExtension));
|
|
30
|
+
function setInfo(raw) {
|
|
31
|
+
const { count, width, height } = (raw ?? {});
|
|
32
|
+
const valid = (v, max) => Number.isInteger(v) && v > 0 && v <= max;
|
|
33
|
+
return valid(count, MAX_FRAMES) && valid(width, 8192) && valid(height, 8192) ? { count, width, height } : null;
|
|
34
|
+
}
|
|
35
|
+
/** The sequence a saved value really describes, or null. Never throws on what a page document holds. */
|
|
36
|
+
function normalizeFrameSequence(raw) {
|
|
37
|
+
const value = (typeof raw === 'string' ? parseJson(raw) : raw);
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return null;
|
|
40
|
+
const wide = setInfo(value.wide);
|
|
41
|
+
if (!wide || typeof value.base !== 'string' || !SAFE_BASE.test(value.base) || !ALL_EXTENSIONS.has(String(value.ext)))
|
|
42
|
+
return null;
|
|
43
|
+
const phone = setInfo(value.phone);
|
|
44
|
+
return {
|
|
45
|
+
base: value.base.replace(/\/+$/, ''),
|
|
46
|
+
ext: value.ext,
|
|
47
|
+
wide,
|
|
48
|
+
...(phone ? { phone } : {}),
|
|
49
|
+
...(Number.isFinite(value.bytes) && value.bytes > 0 ? { bytes: value.bytes } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function parseJson(raw) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** The name a picture is stored under: its place in the set, from 1. */
|
|
61
|
+
function frameFileName(index, ext) {
|
|
62
|
+
return `${String(index + 1).padStart(PAD, '0')}.${ext}`;
|
|
63
|
+
}
|
|
64
|
+
/** Picture `index` (from 0) of a set. */
|
|
65
|
+
function frameUrl(sequence, set, index) {
|
|
66
|
+
return `${sequence.base}/${set}/${frameFileName(index, sequence.ext)}`;
|
|
67
|
+
}
|
|
68
|
+
/** The first picture of each set, for a poster's `srcset`; '' when there is only one set. */
|
|
69
|
+
function phoneFrameSrcset(sequence) {
|
|
70
|
+
if (!sequence.phone)
|
|
71
|
+
return '';
|
|
72
|
+
return `${frameUrl(sequence, FrameSet.Phone, 0)} ${sequence.phone.width}w, ${frameUrl(sequence, FrameSet.Wide, 0)} ${sequence.wide.width}w`;
|
|
73
|
+
}
|
|
74
|
+
/** Which set plays: the phone's on a phone, when there is one. */
|
|
75
|
+
function frameSetFor(sequence, phone) {
|
|
76
|
+
return phone && sequence.phone ? { set: FrameSet.Phone, info: sequence.phone } : { set: FrameSet.Wide, info: sequence.wide };
|
|
77
|
+
}
|
|
78
|
+
/** The picture at `played` % of a set of `count`. */
|
|
79
|
+
function frameIndexAt(played, count) {
|
|
80
|
+
if (count <= 1)
|
|
81
|
+
return 0;
|
|
82
|
+
return Math.round((Math.min(100, Math.max(0, played)) / 100) * (count - 1));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The order to fetch a set in: the two ends, then every 16th, every 8th and so on down to all of them. Any scroll position
|
|
86
|
+
* then has a near picture almost at once, and the movement sharpens as the rest arrive. Fetching in sequence would leave a
|
|
87
|
+
* visitor who scrolls fast looking at the first frames.
|
|
88
|
+
*/
|
|
89
|
+
function frameLoadOrder(count) {
|
|
90
|
+
const order = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const add = (i) => {
|
|
93
|
+
if (i >= 0 && i < count && !seen.has(i)) {
|
|
94
|
+
seen.add(i);
|
|
95
|
+
order.push(i);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
add(0);
|
|
99
|
+
add(count - 1);
|
|
100
|
+
for (let stride = 16; stride >= 1; stride /= 2) {
|
|
101
|
+
for (let i = 0; i < count; i += stride)
|
|
102
|
+
add(i);
|
|
103
|
+
}
|
|
104
|
+
return order;
|
|
105
|
+
}
|
|
106
|
+
/** The loaded picture nearest `index`, or -1 when none has arrived. Ties go backward: the picture already passed. */
|
|
107
|
+
function nearestLoadedFrame(loaded, index) {
|
|
108
|
+
for (let distance = 0; distance < loaded.length; distance++) {
|
|
109
|
+
if (loaded[index - distance])
|
|
110
|
+
return index - distance;
|
|
111
|
+
if (loaded[index + distance])
|
|
112
|
+
return index + distance;
|
|
113
|
+
}
|
|
114
|
+
return -1;
|
|
115
|
+
}
|
|
116
|
+
/** The longest side of a wide picture and of a phone picture. A phone's is not its width: a landscape video covers a
|
|
117
|
+
* tall stage by its height, so a picture sized to the phone's width would be enlarged five times. */
|
|
118
|
+
const WIDE_LONG_SIDE = 1600;
|
|
119
|
+
const PHONE_LONG_SIDE = 1100;
|
|
120
|
+
/** How many pictures a second of video becomes, before the cap. */
|
|
121
|
+
const VIDEO_FRAMES_PER_SECOND = 24;
|
|
122
|
+
/** A video's cap: the pictures a scroll can show, at a weight a page can carry. A set of pictures is the author's own count. */
|
|
123
|
+
const VIDEO_MAX_FRAMES = 180;
|
|
124
|
+
/** `width`×`height` fitted inside `longSide`, never enlarged. */
|
|
125
|
+
function fittedFrameSize(width, height, longSide) {
|
|
126
|
+
const scale = Math.min(1, longSide / Math.max(width, height));
|
|
127
|
+
return { width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)) };
|
|
128
|
+
}
|
|
129
|
+
/** How many pictures a video of `seconds` becomes. */
|
|
130
|
+
function videoFrameCount(seconds) {
|
|
131
|
+
return Math.min(VIDEO_MAX_FRAMES, Math.max(2, Math.ceil(seconds * VIDEO_FRAMES_PER_SECOND)));
|
|
132
|
+
}
|
|
133
|
+
/** Which of `sourceCount` pictures make a set of `count`, spread evenly, first and last always in. */
|
|
134
|
+
function sampledFrameIndexes(sourceCount, count) {
|
|
135
|
+
if (count >= sourceCount)
|
|
136
|
+
return Array.from({ length: sourceCount }, (_, i) => i);
|
|
137
|
+
if (count <= 1)
|
|
138
|
+
return [0];
|
|
139
|
+
return Array.from({ length: count }, (_, i) => Math.round((i * (sourceCount - 1)) / (count - 1)));
|
|
140
|
+
}
|
|
141
|
+
/** A phone plays half the pictures: it has less memory, and half is plenty on a small screen. */
|
|
142
|
+
function phoneFrameCount(wideCount) {
|
|
143
|
+
return Math.max(2, Math.ceil(wideCount / 2));
|
|
144
|
+
}
|
|
145
|
+
const NATURAL = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
|
|
146
|
+
/** Rendered frames in their real order: `frame2` before `frame10`. */
|
|
147
|
+
function sortFrameFiles(files) {
|
|
148
|
+
return [...files].sort((a, b) => NATURAL.compare(a.name, b.name));
|
|
149
|
+
}
|
|
150
|
+
/** A sequence's weight, for the author. */
|
|
151
|
+
function frameSummary(sequence) {
|
|
152
|
+
return { frames: sequence.wide.count, megabytes: sequence.bytes ? Math.round((sequence.bytes / (1024 * 1024)) * 10) / 10 : null };
|
|
153
|
+
}
|
|
154
|
+
var FramesClass;
|
|
155
|
+
(function (FramesClass) {
|
|
156
|
+
FramesClass["Canvas"] = "iox-video__frames";
|
|
157
|
+
})(FramesClass || (FramesClass = {}));
|
|
158
|
+
const LOAD_CONCURRENCY = 6;
|
|
159
|
+
const NEAR_MARGIN = '100% 0px';
|
|
160
|
+
/** Under these the visitor keeps the poster: nobody on a metered 2G connection asked to download a film. */
|
|
161
|
+
const SLOW_CONNECTIONS = new Set(['slow-2g', '2g']);
|
|
162
|
+
/**
|
|
163
|
+
* The player: draws the picture nearest what the scroll asked for on a canvas laid over the poster, and nothing until a
|
|
164
|
+
* first picture has arrived, so the poster is what a visitor sees until then, and for good if the pictures never come.
|
|
165
|
+
* Browser only. It writes and never reads, so the driver can call it in its write phase.
|
|
166
|
+
*/
|
|
167
|
+
class FramePlayer {
|
|
168
|
+
constructor(el, sequence, options = {}) {
|
|
169
|
+
this.el = el;
|
|
170
|
+
this.sequence = sequence;
|
|
171
|
+
this.observer = null;
|
|
172
|
+
this.canvas = null;
|
|
173
|
+
this.context = null;
|
|
174
|
+
this.wanted = 0;
|
|
175
|
+
this.drawn = -1;
|
|
176
|
+
this.nextToLoad = 0;
|
|
177
|
+
this.loading = 0;
|
|
178
|
+
this.started = false;
|
|
179
|
+
this.destroyed = false;
|
|
180
|
+
this.pending = new Set();
|
|
181
|
+
this.host = options.host ?? window;
|
|
182
|
+
const { set, info } = frameSetFor(sequence, !!options.phone);
|
|
183
|
+
this.set = set;
|
|
184
|
+
this.info = info;
|
|
185
|
+
this.images = new Array(info.count).fill(null);
|
|
186
|
+
this.order = frameLoadOrder(info.count);
|
|
187
|
+
if (!this.allowed())
|
|
188
|
+
return;
|
|
189
|
+
const Observer = this.host.IntersectionObserver;
|
|
190
|
+
if (!Observer) {
|
|
191
|
+
this.start();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.observer = new Observer(entries => {
|
|
195
|
+
if (entries.some(entry => entry.isIntersecting))
|
|
196
|
+
this.start();
|
|
197
|
+
}, { rootMargin: NEAR_MARGIN });
|
|
198
|
+
this.observer.observe(options.near ?? el);
|
|
199
|
+
}
|
|
200
|
+
/** Stand at `played` %. */
|
|
201
|
+
seek(played) {
|
|
202
|
+
this.wanted = frameIndexAt(played, this.info.count);
|
|
203
|
+
this.draw();
|
|
204
|
+
}
|
|
205
|
+
/** Whether a re-render took the canvas away with the markup it sat in. */
|
|
206
|
+
get detached() {
|
|
207
|
+
return !!this.canvas && !this.canvas.isConnected;
|
|
208
|
+
}
|
|
209
|
+
destroy() {
|
|
210
|
+
this.destroyed = true;
|
|
211
|
+
this.observer?.disconnect();
|
|
212
|
+
this.observer = null;
|
|
213
|
+
for (const image of this.pending) {
|
|
214
|
+
image.onload = null;
|
|
215
|
+
image.onerror = null;
|
|
216
|
+
image.src = '';
|
|
217
|
+
}
|
|
218
|
+
this.pending.clear();
|
|
219
|
+
this.canvas?.remove();
|
|
220
|
+
this.canvas = null;
|
|
221
|
+
this.context = null;
|
|
222
|
+
}
|
|
223
|
+
allowed() {
|
|
224
|
+
const connection = this.host.navigator?.connection;
|
|
225
|
+
return !connection?.saveData && !SLOW_CONNECTIONS.has(connection?.effectiveType ?? '');
|
|
226
|
+
}
|
|
227
|
+
start() {
|
|
228
|
+
if (this.started)
|
|
229
|
+
return;
|
|
230
|
+
this.started = true;
|
|
231
|
+
this.observer?.disconnect();
|
|
232
|
+
this.observer = null;
|
|
233
|
+
this.pump();
|
|
234
|
+
}
|
|
235
|
+
pump() {
|
|
236
|
+
while (!this.destroyed && this.loading < LOAD_CONCURRENCY && this.nextToLoad < this.order.length) {
|
|
237
|
+
this.load(this.order[this.nextToLoad++]);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
load(index) {
|
|
241
|
+
const image = this.host.document.createElement('img');
|
|
242
|
+
image.decoding = 'async';
|
|
243
|
+
this.loading++;
|
|
244
|
+
this.pending.add(image);
|
|
245
|
+
const done = (arrived) => {
|
|
246
|
+
this.pending.delete(image);
|
|
247
|
+
this.loading--;
|
|
248
|
+
if (this.destroyed)
|
|
249
|
+
return;
|
|
250
|
+
if (arrived) {
|
|
251
|
+
this.images[index] = image;
|
|
252
|
+
this.draw();
|
|
253
|
+
}
|
|
254
|
+
this.pump();
|
|
255
|
+
};
|
|
256
|
+
// Decoded before it counts, or the first draw of each picture decodes on the driver's frame and the scroll stutters.
|
|
257
|
+
image.onload = () => {
|
|
258
|
+
const decoded = image.decode?.();
|
|
259
|
+
if (decoded)
|
|
260
|
+
void decoded.then(() => done(true), () => done(true));
|
|
261
|
+
else
|
|
262
|
+
done(true);
|
|
263
|
+
};
|
|
264
|
+
// A picture that failed is a gap: its neighbours stand in.
|
|
265
|
+
image.onerror = () => done(false);
|
|
266
|
+
image.src = frameUrl(this.sequence, this.set, index);
|
|
267
|
+
}
|
|
268
|
+
draw() {
|
|
269
|
+
const index = nearestLoadedFrame(this.images, this.wanted);
|
|
270
|
+
if (index < 0 || index === this.drawn)
|
|
271
|
+
return;
|
|
272
|
+
const image = this.images[index];
|
|
273
|
+
if (!this.ensureCanvas(image))
|
|
274
|
+
return;
|
|
275
|
+
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
276
|
+
this.context.drawImage(image, 0, 0, this.canvas.width, this.canvas.height);
|
|
277
|
+
this.drawn = index;
|
|
278
|
+
}
|
|
279
|
+
ensureCanvas(image) {
|
|
280
|
+
if (this.canvas)
|
|
281
|
+
return !!this.context;
|
|
282
|
+
const canvas = this.host.document.createElement('canvas');
|
|
283
|
+
canvas.className = FramesClass.Canvas;
|
|
284
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
285
|
+
canvas.width = image.naturalWidth || this.info.width;
|
|
286
|
+
canvas.height = image.naturalHeight || this.info.height;
|
|
287
|
+
this.context = canvas.getContext('2d');
|
|
288
|
+
if (!this.context)
|
|
289
|
+
return false;
|
|
290
|
+
this.canvas = canvas;
|
|
291
|
+
this.el.appendChild(canvas);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
3
296
|
/**
|
|
4
297
|
* VIDEO (CC3 — complex-components plan §7b, board 5). A LEAF type with two sources:
|
|
5
298
|
* - an uploaded FILE renders as a real `<video>` (poster, `preload="metadata"`, captions, controls);
|
|
@@ -38,6 +331,8 @@ var VideoTrait;
|
|
|
38
331
|
VideoTrait["Captions"] = "captions";
|
|
39
332
|
VideoTrait["CaptionsLang"] = "captionsLang";
|
|
40
333
|
VideoTrait["Consent"] = "consentCategory";
|
|
334
|
+
/** Pictures the scroll plays (SC4): a `FrameSequence`. Written by the Scene panel, never typed. */
|
|
335
|
+
VideoTrait["Frames"] = "frames";
|
|
41
336
|
})(VideoTrait || (VideoTrait = {}));
|
|
42
337
|
/** What a plan renders. */
|
|
43
338
|
var VideoKind;
|
|
@@ -48,6 +343,8 @@ var VideoKind;
|
|
|
48
343
|
VideoKind["Facade"] = "facade";
|
|
49
344
|
/** No address yet: the poster blank (DS3) and nothing to play. */
|
|
50
345
|
VideoKind["Empty"] = "empty";
|
|
346
|
+
/** Pictures the scroll plays: the poster, and the driver's canvas over it. No player, no controls. */
|
|
347
|
+
VideoKind["Frames"] = "frames";
|
|
51
348
|
})(VideoKind || (VideoKind = {}));
|
|
52
349
|
const VIDEO_PROVIDER_LABEL = {
|
|
53
350
|
[VideoSource.YouTube]: 'YouTube',
|
|
@@ -160,6 +457,9 @@ function videoConsentCategory(node, source = videoSourceOf(videoProp(node, Video
|
|
|
160
457
|
* studio's own image (or the thumbnail the server fetched and stored for it), never a request to a third party.
|
|
161
458
|
*/
|
|
162
459
|
function planVideo(node, consent) {
|
|
460
|
+
const frames = normalizeFrameSequence(videoProp(node, VideoTrait.Frames));
|
|
461
|
+
if (frames)
|
|
462
|
+
return planFrames(node, frames);
|
|
163
463
|
const rawSrc = str(videoProp(node, VideoTrait.Src));
|
|
164
464
|
const source = videoSourceOf(rawSrc, videoProp(node, VideoTrait.Source));
|
|
165
465
|
const ref = source === VideoSource.File ? null : parseVideoUrl(rawSrc);
|
|
@@ -187,6 +487,32 @@ function planVideo(node, consent) {
|
|
|
187
487
|
granted,
|
|
188
488
|
embedUrl: kind === VideoKind.Facade && granted && ref ? videoEmbedUrl(ref) : null,
|
|
189
489
|
providerLabel: ref ? VIDEO_PROVIDER_LABEL[ref.source] : '',
|
|
490
|
+
frames: null,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/** Pictures win over any address the node also holds: it was turned into them. No player, no consent, nothing to press. */
|
|
494
|
+
function planFrames(node, frames) {
|
|
495
|
+
const poster = str(videoProp(node, VideoTrait.Poster));
|
|
496
|
+
return {
|
|
497
|
+
kind: VideoKind.Frames,
|
|
498
|
+
source: VideoSource.File,
|
|
499
|
+
ref: null,
|
|
500
|
+
src: '',
|
|
501
|
+
poster: poster && !isThirdPartyPoster(poster) ? poster : frameUrl(frames, FrameSet.Wide, 0),
|
|
502
|
+
title: str(videoProp(node, VideoTrait.Title)),
|
|
503
|
+
mode: VideoMode.Inline,
|
|
504
|
+
background: false,
|
|
505
|
+
autoplay: false,
|
|
506
|
+
muted: false,
|
|
507
|
+
loop: false,
|
|
508
|
+
controls: false,
|
|
509
|
+
captions: '',
|
|
510
|
+
captionsLang: 'en',
|
|
511
|
+
consentCategory: null,
|
|
512
|
+
granted: true,
|
|
513
|
+
embedUrl: null,
|
|
514
|
+
providerLabel: '',
|
|
515
|
+
frames,
|
|
190
516
|
};
|
|
191
517
|
}
|
|
192
518
|
const THIRD_PARTY_POSTER_HOSTS = ['ytimg.com', 'youtube.com', 'vimeocdn.com', 'vimeo.com', 'googleusercontent.com'];
|
|
@@ -270,6 +596,8 @@ var VideoAttribute;
|
|
|
270
596
|
VideoAttribute["Playing"] = "data-iox-video-playing";
|
|
271
597
|
/** The consent prompt is showing. */
|
|
272
598
|
VideoAttribute["Asking"] = "data-iox-video-asking";
|
|
599
|
+
/** The pictures the scroll plays, as JSON: what the driver reads, so it needs no node tree. */
|
|
600
|
+
VideoAttribute["Frames"] = "data-iox-video-frames";
|
|
273
601
|
})(VideoAttribute || (VideoAttribute = {}));
|
|
274
602
|
var VideoIcon;
|
|
275
603
|
(function (VideoIcon) {
|
|
@@ -294,6 +622,8 @@ function videoRootAttributes(plan) {
|
|
|
294
622
|
attrs[VideoAttribute.Title] = plan.title;
|
|
295
623
|
if (!plan.poster)
|
|
296
624
|
attrs[VideoAttribute.PosterEmpty] = '';
|
|
625
|
+
if (plan.frames)
|
|
626
|
+
attrs[VideoAttribute.Frames] = JSON.stringify(plan.frames);
|
|
297
627
|
return attrs;
|
|
298
628
|
}
|
|
299
629
|
const iconHtml = (cls) => `<i class="${cls}" aria-hidden="true"></i>`;
|
|
@@ -302,6 +632,15 @@ const iconHtml = (cls) => `<i class="${cls}" aria-hidden="true"></i>`;
|
|
|
302
632
|
function videoInnerHtml(plan, options) {
|
|
303
633
|
const { esc } = options;
|
|
304
634
|
const words = options.words ?? VIDEO_WORDS.en;
|
|
635
|
+
// The same on the canvas and on the site: the first picture is real content, so it is fetched at once, named, and the
|
|
636
|
+
// size a phone needs. A driver's canvas covers it when pictures arrive; with no script it is what there is.
|
|
637
|
+
if (plan.kind === VideoKind.Frames) {
|
|
638
|
+
const own = !!plan.frames && !isAuthoredPoster(plan);
|
|
639
|
+
const srcset = own ? phoneFrameSrcset(plan.frames) : '';
|
|
640
|
+
// Its real size, so where a Scene flows (reduced motion, no script) the picture holds its height before it loads.
|
|
641
|
+
const size = own ? ` width="${plan.frames.wide.width}" height="${plan.frames.wide.height}"` : '';
|
|
642
|
+
return `<img class="${VideoClass.Poster}" src="${esc(plan.poster)}"${srcset ? ` srcset="${esc(srcset)}" sizes="100vw"` : ''}${size} alt="${esc(plan.title)}" decoding="async">`;
|
|
643
|
+
}
|
|
305
644
|
const poster = plan.poster ? `<img class="${VideoClass.Poster}" src="${esc(plan.poster)}" alt="" loading="lazy" decoding="async">` : '';
|
|
306
645
|
const label = esc(videoPlayLabel(plan, words));
|
|
307
646
|
const playSpan = `<span class="${VideoClass.Play}" aria-hidden="true">${iconHtml(VideoIcon.Play)}</span>`;
|
|
@@ -341,6 +680,7 @@ function videoInnerHtml(plan, options) {
|
|
|
341
680
|
`</div></div>`;
|
|
342
681
|
return `${poster}<button type="button" class="${VideoClass.Play}" aria-label="${label}">${iconHtml(VideoIcon.Play)}</button>${badge}${prompt}`;
|
|
343
682
|
}
|
|
683
|
+
const isAuthoredPoster = (plan) => !!plan.frames && plan.poster !== frameUrl(plan.frames, FrameSet.Wide, 0);
|
|
344
684
|
/** Serialise attributes for a string renderer (a present boolean attribute is written bare). */
|
|
345
685
|
function serializeVideoAttributes(attrs, esc) {
|
|
346
686
|
return Object.entries(attrs).map(([name, value]) => (value === '' ? ` ${name}` : ` ${name}="${esc(value)}"`)).join('');
|
|
@@ -567,5 +907,5 @@ function videoRuntimeOf(el) {
|
|
|
567
907
|
* Generated bundle index. Do not edit.
|
|
568
908
|
*/
|
|
569
909
|
|
|
570
|
-
export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, VIDEO_EMBED_ORIGINS, VIDEO_NODE_TYPE, VIDEO_PROVIDER_COMPANY, VIDEO_PROVIDER_LABEL, VIDEO_WORDS, VideoAttribute, VideoClass, VideoIcon, VideoKind, VideoMode, VideoRuntime, VideoSource, VideoTrait, attachVideoAt, attachVideos, documentConsentTransport, isThirdPartyPoster, parseVideoUrl, planVideo, readConsentRecord, serializeVideoAttributes, videoConsentCategory, videoEmbedUrl, videoInnerHtml, videoPlayLabel, videoPosterFileName, videoProp, videoRefOf, videoRootAttributes, videoRuntimeOf, videoSourceOf, videoThumbnailRequest, videoWords };
|
|
910
|
+
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 };
|
|
571
911
|
//# sourceMappingURL=vectoriox-iox-ui-media-core.mjs.map
|