visualfries 0.1.10101 → 0.1.10110
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/dist/SceneBuilder.svelte.d.ts +3 -3
- package/dist/SceneBuilder.svelte.js +34 -10
- package/dist/commands/RenderFrameCommand.d.ts +1 -0
- package/dist/commands/RenderFrameCommand.js +31 -11
- package/dist/fonts/fontDiscovery.d.ts +9 -0
- package/dist/fonts/fontDiscovery.js +202 -0
- package/dist/layers/Layer.svelte.d.ts +1 -0
- package/dist/layers/Layer.svelte.js +30 -0
- package/dist/managers/RenderManager.d.ts +4 -0
- package/dist/managers/RenderManager.js +16 -0
- package/dist/schemas/runtime/deterministic.d.ts +11 -0
- package/dist/schemas/runtime/deterministic.js +6 -0
- package/dist/schemas/runtime/index.d.ts +2 -2
- package/dist/schemas/runtime/index.js +1 -1
- package/dist/schemas/runtime/types.d.ts +4 -3
- package/dist/utils/document.d.ts +2 -1
- package/dist/utils/document.js +113 -24
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import { DomManager } from './managers/DomManager.js';
|
|
|
8
8
|
import { AppManager } from './managers/AppManager.svelte.js';
|
|
9
9
|
import { ComponentsManager } from './managers/ComponentsManager.svelte.js';
|
|
10
10
|
import type { EventMap, EventType, EventPayload, BuilderState, ISceneBuilder } from './';
|
|
11
|
-
import type { DeterministicFrameProvider, DeterministicMediaConfig, DeterministicDiagnosticsReport, RenderFrameRangeOptions, RenderFrameRangeSummary } from './';
|
|
11
|
+
import type { DeterministicFrameProvider, DeterministicMediaConfig, DeterministicDiagnosticsReport, FrameImageEncodingOptions, RenderFrameRangeOptions, RenderFrameRangeSummary } from './';
|
|
12
12
|
import { MediaManager } from './managers/MediaManager.js';
|
|
13
13
|
import { DeterministicMediaManager } from './managers/DeterministicMediaManager.js';
|
|
14
14
|
import { LayersManager } from './managers/LayersManager.svelte.js';
|
|
@@ -5377,7 +5377,7 @@ export declare class SceneBuilder implements ISceneBuilder {
|
|
|
5377
5377
|
getDeterministicFrameProvider(): DeterministicFrameProvider | null;
|
|
5378
5378
|
getDeterministicMediaConfig(): DeterministicMediaConfig;
|
|
5379
5379
|
getDiagnosticsReport(): DeterministicDiagnosticsReport | null;
|
|
5380
|
-
seekAndRenderFrame(time: number, target?: PIXI.DisplayObject | PIXI.RenderTexture, format?: string, quality?: number): Promise<string | ArrayBuffer | Blob>;
|
|
5380
|
+
seekAndRenderFrame(time: number, target?: PIXI.DisplayObject | PIXI.RenderTexture, format?: string, quality?: number, imageOptions?: FrameImageEncodingOptions): Promise<string | ArrayBuffer | Blob>;
|
|
5381
5381
|
/**
|
|
5382
5382
|
* Check if seeking to a specific time would result in visual changes
|
|
5383
5383
|
* without actually extracting frame data.
|
|
@@ -5402,7 +5402,7 @@ export declare class SceneBuilder implements ISceneBuilder {
|
|
|
5402
5402
|
* }
|
|
5403
5403
|
*/
|
|
5404
5404
|
isSceneDirty(time: number): Promise<boolean>;
|
|
5405
|
-
renderFrame(target?: PIXI.DisplayObject | PIXI.RenderTexture, format?: string, quality?: number): Promise<string | ArrayBuffer | Blob>;
|
|
5405
|
+
renderFrame(target?: PIXI.DisplayObject | PIXI.RenderTexture, format?: string, quality?: number, imageOptions?: FrameImageEncodingOptions): Promise<string | ArrayBuffer | Blob>;
|
|
5406
5406
|
renderFrameRange(options: RenderFrameRangeOptions): Promise<RenderFrameRangeSummary>;
|
|
5407
5407
|
log(message: string): void;
|
|
5408
5408
|
play(changeState?: boolean): void;
|
|
@@ -3,6 +3,7 @@ import { gsap } from 'gsap';
|
|
|
3
3
|
import { ComponentShape } from './';
|
|
4
4
|
import { buildCharactersListFromComponentsAndSubtitles, changeIdDeep } from './utils/utils.js';
|
|
5
5
|
import { loadFonts } from './utils/document.js';
|
|
6
|
+
import { discoverRequiredFontVariants } from './fonts/fontDiscovery.js';
|
|
6
7
|
import { CommandType } from './commands/CommandTypes.js';
|
|
7
8
|
import { CommandRunner } from './commands/CommandRunner.js';
|
|
8
9
|
import { StateManager } from './managers/StateManager.svelte.js';
|
|
@@ -172,7 +173,8 @@ export class SceneBuilder {
|
|
|
172
173
|
this.domManager.scale(clampedScale);
|
|
173
174
|
}
|
|
174
175
|
async loadFonts(fonts) {
|
|
175
|
-
|
|
176
|
+
const variants = discoverRequiredFontVariants(this.sceneData, fonts);
|
|
177
|
+
return await loadFonts(fonts, variants);
|
|
176
178
|
}
|
|
177
179
|
async initialize() {
|
|
178
180
|
if (this.initialized) {
|
|
@@ -183,9 +185,7 @@ export class SceneBuilder {
|
|
|
183
185
|
this.renderTicker = () => {
|
|
184
186
|
this.render();
|
|
185
187
|
};
|
|
186
|
-
|
|
187
|
-
await this.loadFonts(this.fonts);
|
|
188
|
-
}
|
|
188
|
+
await this.loadFonts(this.fonts);
|
|
189
189
|
this.layersManager.setAppManager(this.appManager);
|
|
190
190
|
await this.appManager.initialize();
|
|
191
191
|
if (this.stateManager.scale !== 1) {
|
|
@@ -310,14 +310,14 @@ export class SceneBuilder {
|
|
|
310
310
|
getDiagnosticsReport() {
|
|
311
311
|
return this.deterministicMediaManager.getDiagnosticsReport();
|
|
312
312
|
}
|
|
313
|
-
async seekAndRenderFrame(time, target, format = 'png', quality = 1) {
|
|
313
|
+
async seekAndRenderFrame(time, target, format = 'png', quality = 1, imageOptions) {
|
|
314
314
|
await this.seek(time);
|
|
315
315
|
// In server mode SeekCommand performs awaited render preparation.
|
|
316
316
|
// Keep client render behavior unchanged.
|
|
317
317
|
if (this.environment !== 'server') {
|
|
318
318
|
this.render();
|
|
319
319
|
}
|
|
320
|
-
const frame = await this.renderFrame(target, format, quality);
|
|
320
|
+
const frame = await this.renderFrame(target, format, quality, imageOptions);
|
|
321
321
|
return frame;
|
|
322
322
|
}
|
|
323
323
|
/**
|
|
@@ -354,8 +354,14 @@ export class SceneBuilder {
|
|
|
354
354
|
this.render();
|
|
355
355
|
return wasDirty || this.stateManager.isDirty;
|
|
356
356
|
}
|
|
357
|
-
async renderFrame(target, format = 'png', quality = 1) {
|
|
358
|
-
const frame = (await this.run(CommandType.RENDER_FRAME, {
|
|
357
|
+
async renderFrame(target, format = 'png', quality = 1, imageOptions) {
|
|
358
|
+
const frame = (await this.run(CommandType.RENDER_FRAME, {
|
|
359
|
+
target,
|
|
360
|
+
format,
|
|
361
|
+
quality,
|
|
362
|
+
imageFormat: imageOptions?.imageFormat,
|
|
363
|
+
imageQuality: imageOptions?.imageQuality
|
|
364
|
+
}));
|
|
359
365
|
if (!frame) {
|
|
360
366
|
throw new Error('Rendering frame failed');
|
|
361
367
|
}
|
|
@@ -367,6 +373,10 @@ export class SceneBuilder {
|
|
|
367
373
|
}
|
|
368
374
|
const format = options.format ?? 'blob';
|
|
369
375
|
const quality = options.quality ?? 1;
|
|
376
|
+
const imageOptions = {
|
|
377
|
+
imageFormat: options.imageFormat,
|
|
378
|
+
imageQuality: options.imageQuality
|
|
379
|
+
};
|
|
370
380
|
const skipDuplicates = options.skipDuplicates ?? false;
|
|
371
381
|
const fromFrame = Math.max(0, Math.floor(options.fromFrame));
|
|
372
382
|
const toFrame = Math.max(fromFrame, Math.floor(options.toFrame));
|
|
@@ -374,6 +384,7 @@ export class SceneBuilder {
|
|
|
374
384
|
let framesSkipped = 0;
|
|
375
385
|
let aborted = false;
|
|
376
386
|
let previousFrame = null;
|
|
387
|
+
let previousMimeType;
|
|
377
388
|
for (let frameIndex = fromFrame; frameIndex < toFrame; frameIndex += 1) {
|
|
378
389
|
if (options.signal?.aborted) {
|
|
379
390
|
aborted = true;
|
|
@@ -381,23 +392,35 @@ export class SceneBuilder {
|
|
|
381
392
|
}
|
|
382
393
|
let frame;
|
|
383
394
|
let isDuplicate = false;
|
|
395
|
+
let mimeType;
|
|
384
396
|
const frameTime = frameIndex / this.fps;
|
|
385
397
|
if (skipDuplicates) {
|
|
386
398
|
const isDirty = await this.isSceneDirty(frameTime);
|
|
387
399
|
if (!isDirty && previousFrame) {
|
|
388
400
|
frame = previousFrame;
|
|
401
|
+
mimeType = previousMimeType;
|
|
389
402
|
isDuplicate = true;
|
|
390
403
|
framesSkipped += 1;
|
|
391
404
|
}
|
|
392
405
|
else {
|
|
393
|
-
frame = await this.seekAndRenderFrame(frameTime, undefined, format, quality);
|
|
406
|
+
frame = await this.seekAndRenderFrame(frameTime, undefined, format, quality, imageOptions);
|
|
394
407
|
previousFrame = frame;
|
|
395
408
|
}
|
|
396
409
|
}
|
|
397
410
|
else {
|
|
398
|
-
frame = await this.seekAndRenderFrame(frameTime, undefined, format, quality);
|
|
411
|
+
frame = await this.seekAndRenderFrame(frameTime, undefined, format, quality, imageOptions);
|
|
399
412
|
previousFrame = frame;
|
|
400
413
|
}
|
|
414
|
+
if (!mimeType && frame instanceof Blob) {
|
|
415
|
+
mimeType = frame.type || undefined;
|
|
416
|
+
}
|
|
417
|
+
if (!mimeType && format === 'blob') {
|
|
418
|
+
const imageFormat = imageOptions.imageFormat ?? 'png';
|
|
419
|
+
mimeType = imageFormat === 'jpg' || imageFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
420
|
+
}
|
|
421
|
+
if (!isDuplicate) {
|
|
422
|
+
previousMimeType = mimeType;
|
|
423
|
+
}
|
|
401
424
|
let released = false;
|
|
402
425
|
const release = () => {
|
|
403
426
|
if (released) {
|
|
@@ -409,6 +432,7 @@ export class SceneBuilder {
|
|
|
409
432
|
frameIndex,
|
|
410
433
|
frame,
|
|
411
434
|
isDuplicate,
|
|
435
|
+
mimeType,
|
|
412
436
|
release
|
|
413
437
|
});
|
|
414
438
|
release();
|
|
@@ -17,5 +17,6 @@ export declare class RenderFrameCommand implements Command<string | ArrayBuffer
|
|
|
17
17
|
appManager: AppManager;
|
|
18
18
|
deterministicMediaManager?: DeterministicMediaManager;
|
|
19
19
|
});
|
|
20
|
+
private resolveBlobMimeType;
|
|
20
21
|
execute(args: unknown): Promise<string | ArrayBuffer | Blob | null>;
|
|
21
22
|
}
|
|
@@ -3,10 +3,13 @@ import { StateManager } from '../managers/StateManager.svelte.js';
|
|
|
3
3
|
import { DomManager } from '../managers/DomManager.js';
|
|
4
4
|
import { AppManager } from '../managers/AppManager.svelte.js';
|
|
5
5
|
import { DeterministicMediaManager } from '../managers/DeterministicMediaManager.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { RenderFrameEncodingError } from '../schemas/runtime/deterministic.js';
|
|
7
|
+
const renderFrameSchema = z.object({
|
|
8
|
+
format: z.enum(['arraybuffer', 'blob', 'png', 'jpg', 'jpeg']).prefault('png'),
|
|
9
|
+
quality: z.number().min(0).max(1).prefault(1),
|
|
10
|
+
target: z.any().optional(),
|
|
11
|
+
imageFormat: z.enum(['png', 'jpg', 'jpeg']).optional(),
|
|
12
|
+
imageQuality: z.number().min(0).max(1).optional()
|
|
10
13
|
});
|
|
11
14
|
export class RenderFrameCommand {
|
|
12
15
|
sceneState;
|
|
@@ -22,12 +25,21 @@ export class RenderFrameCommand {
|
|
|
22
25
|
this.appManager = cradle.appManager;
|
|
23
26
|
this.deterministicMediaManager = cradle.deterministicMediaManager;
|
|
24
27
|
}
|
|
28
|
+
resolveBlobMimeType(imageFormat) {
|
|
29
|
+
if (!imageFormat) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (imageFormat === 'jpg' || imageFormat === 'jpeg') {
|
|
33
|
+
return 'image/jpeg';
|
|
34
|
+
}
|
|
35
|
+
return 'image/png';
|
|
36
|
+
}
|
|
25
37
|
async execute(args) {
|
|
26
|
-
const check =
|
|
38
|
+
const check = renderFrameSchema.safeParse(args);
|
|
27
39
|
if (!check.success) {
|
|
28
40
|
return null;
|
|
29
41
|
}
|
|
30
|
-
const { format, quality, target } = check.data;
|
|
42
|
+
const { format, quality, target, imageFormat, imageQuality } = check.data;
|
|
31
43
|
const currentDeterministicFingerprint = this.deterministicMediaManager?.isEnabled() ? this.deterministicMediaManager.getFingerprint() : '';
|
|
32
44
|
// Server optimization: Return cached frame if nothing changed visually and render args match
|
|
33
45
|
if (this.sceneState.environment === 'server' && !this.sceneState.isDirty) {
|
|
@@ -35,7 +47,9 @@ export class RenderFrameCommand {
|
|
|
35
47
|
// Check if render args match current args
|
|
36
48
|
const argsMatch = this.lastRenderArgs.format === format &&
|
|
37
49
|
this.lastRenderArgs.quality === quality &&
|
|
38
|
-
this.lastRenderArgs.target === target
|
|
50
|
+
this.lastRenderArgs.target === target &&
|
|
51
|
+
this.lastRenderArgs.imageFormat === imageFormat &&
|
|
52
|
+
this.lastRenderArgs.imageQuality === imageQuality;
|
|
39
53
|
if (argsMatch && this.lastDeterministicFingerprint === currentDeterministicFingerprint) {
|
|
40
54
|
return this.lastRenderedFrame;
|
|
41
55
|
}
|
|
@@ -59,11 +73,17 @@ export class RenderFrameCommand {
|
|
|
59
73
|
}));
|
|
60
74
|
}
|
|
61
75
|
if (format === 'blob') {
|
|
62
|
-
|
|
76
|
+
const mimeType = this.resolveBlobMimeType(imageFormat);
|
|
77
|
+
const blobQuality = imageQuality ?? quality;
|
|
78
|
+
frame = (await new Promise((resolve, reject) => {
|
|
63
79
|
requestAnimationFrame(() => {
|
|
64
80
|
this.domManager.canvas.toBlob((blob) => {
|
|
81
|
+
if (!blob) {
|
|
82
|
+
reject(new RenderFrameEncodingError(`RenderFrameCommand: canvas.toBlob returned null for format="blob" (imageFormat="${imageFormat ?? 'default'}").`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
65
85
|
resolve(blob);
|
|
66
|
-
});
|
|
86
|
+
}, mimeType, blobQuality);
|
|
67
87
|
});
|
|
68
88
|
}));
|
|
69
89
|
}
|
|
@@ -73,7 +93,7 @@ export class RenderFrameCommand {
|
|
|
73
93
|
? (await new Promise((resolve) => {
|
|
74
94
|
requestAnimationFrame(() => {
|
|
75
95
|
const frame = format === 'jpg' || format === 'jpeg'
|
|
76
|
-
? this.domManager.canvas.toDataURL('image/jpeg', quality)
|
|
96
|
+
? this.domManager.canvas.toDataURL('image/jpeg', imageQuality ?? quality)
|
|
77
97
|
: this.domManager.canvas.toDataURL();
|
|
78
98
|
resolve(frame);
|
|
79
99
|
});
|
|
@@ -90,7 +110,7 @@ export class RenderFrameCommand {
|
|
|
90
110
|
// Cache frame and render args, then clear dirty flag after successful render
|
|
91
111
|
if (this.sceneState.environment === 'server') {
|
|
92
112
|
this.lastRenderedFrame = frame;
|
|
93
|
-
this.lastRenderArgs = { format, quality, target };
|
|
113
|
+
this.lastRenderArgs = { format, quality, target, imageFormat, imageQuality };
|
|
94
114
|
this.lastDeterministicFingerprint = currentDeterministicFingerprint;
|
|
95
115
|
this.sceneState.clearDirty();
|
|
96
116
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FontType, Scene } from '..';
|
|
2
|
+
export type FontVariantDescriptor = {
|
|
3
|
+
family: string;
|
|
4
|
+
weight: number;
|
|
5
|
+
source: 'google' | 'custom';
|
|
6
|
+
fileUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const extractConfiguredFontVariants: (configuredFonts?: FontType[]) => FontVariantDescriptor[];
|
|
9
|
+
export declare const discoverRequiredFontVariants: (sceneData: Scene, configuredFonts?: FontType[]) => FontVariantDescriptor[];
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const NAMED_WEIGHTS = {
|
|
2
|
+
normal: 400,
|
|
3
|
+
regular: 400,
|
|
4
|
+
italic: 400,
|
|
5
|
+
bold: 700,
|
|
6
|
+
bolder: 700,
|
|
7
|
+
lighter: 300
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_WEIGHT = 400;
|
|
10
|
+
const normalizeFamily = (value) => {
|
|
11
|
+
if (typeof value !== 'string') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const trimmed = value.trim().replace(/^['"]|['"]$/g, '');
|
|
15
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
16
|
+
};
|
|
17
|
+
const normalizeWeight = (value) => {
|
|
18
|
+
if (typeof value === 'number' && value >= 100 && value <= 900) {
|
|
19
|
+
return Math.round(value / 100) * 100;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const normalized = value.trim().toLowerCase();
|
|
25
|
+
if (!normalized) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (NAMED_WEIGHTS[normalized] !== undefined) {
|
|
29
|
+
return NAMED_WEIGHTS[normalized];
|
|
30
|
+
}
|
|
31
|
+
const directNumeric = Number.parseInt(normalized, 10);
|
|
32
|
+
if (!Number.isNaN(directNumeric) && directNumeric >= 100 && directNumeric <= 900) {
|
|
33
|
+
return Math.round(directNumeric / 100) * 100;
|
|
34
|
+
}
|
|
35
|
+
const embeddedWeightMatch = normalized.match(/([1-9]00)/);
|
|
36
|
+
if (embeddedWeightMatch?.[1]) {
|
|
37
|
+
return Number.parseInt(embeddedWeightMatch[1], 10);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
const parseWeightsFromVariants = (variants) => {
|
|
42
|
+
if (!Array.isArray(variants)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const weights = new Set();
|
|
46
|
+
for (const variant of variants) {
|
|
47
|
+
const normalizedWeight = normalizeWeight(variant);
|
|
48
|
+
if (normalizedWeight !== null) {
|
|
49
|
+
weights.add(normalizedWeight);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...weights];
|
|
53
|
+
};
|
|
54
|
+
const parseConfiguredFamilyDescriptor = (value, fallbackFamily) => {
|
|
55
|
+
const [familyPart, variantPart] = value.split(':', 2);
|
|
56
|
+
const family = normalizeFamily(familyPart) ?? normalizeFamily(fallbackFamily) ?? fallbackFamily;
|
|
57
|
+
const weights = new Set();
|
|
58
|
+
if (variantPart) {
|
|
59
|
+
const numericMatches = variantPart.matchAll(/([1-9]00)/g);
|
|
60
|
+
for (const match of numericMatches) {
|
|
61
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
62
|
+
if (!Number.isNaN(parsed)) {
|
|
63
|
+
weights.add(parsed);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (weights.size === 0) {
|
|
67
|
+
for (const token of variantPart.split(/[;,]/)) {
|
|
68
|
+
const parsed = normalizeWeight(token);
|
|
69
|
+
if (parsed !== null) {
|
|
70
|
+
weights.add(parsed);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (weights.size === 0) {
|
|
76
|
+
weights.add(DEFAULT_WEIGHT);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
family,
|
|
80
|
+
weights: [...weights]
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const parseConfiguredFonts = (configuredFonts) => {
|
|
84
|
+
const parsedFonts = [];
|
|
85
|
+
for (const configuredFont of configuredFonts) {
|
|
86
|
+
const alias = normalizeFamily(configuredFont.alias);
|
|
87
|
+
const fallbackFamily = alias ?? 'Unnamed Font';
|
|
88
|
+
const configuredFamilyDescriptor = configuredFont.data?.family ?? fallbackFamily;
|
|
89
|
+
const parsedFamilyDescriptor = parseConfiguredFamilyDescriptor(configuredFamilyDescriptor, fallbackFamily);
|
|
90
|
+
const fileUrl = normalizeFamily(configuredFont.src ?? configuredFont.url);
|
|
91
|
+
parsedFonts.push({
|
|
92
|
+
family: parsedFamilyDescriptor.family,
|
|
93
|
+
source: configuredFont.source,
|
|
94
|
+
fileUrl: fileUrl ?? undefined,
|
|
95
|
+
weights: parsedFamilyDescriptor.weights,
|
|
96
|
+
aliases: alias ? [alias] : []
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return parsedFonts;
|
|
100
|
+
};
|
|
101
|
+
const createConfiguredLookup = (configuredFonts) => {
|
|
102
|
+
const lookup = new Map();
|
|
103
|
+
for (const configuredFont of configuredFonts) {
|
|
104
|
+
const keys = [configuredFont.family, ...configuredFont.aliases];
|
|
105
|
+
for (const key of keys) {
|
|
106
|
+
lookup.set(key.toLowerCase(), configuredFont);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lookup;
|
|
110
|
+
};
|
|
111
|
+
const upsertVariant = (variants, variant) => {
|
|
112
|
+
const key = `${variant.family.toLowerCase()}::${variant.weight}`;
|
|
113
|
+
const existing = variants.get(key);
|
|
114
|
+
if (!existing) {
|
|
115
|
+
variants.set(key, variant);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Prefer custom source when available and preserve fileUrl if provided by any input.
|
|
119
|
+
if (existing.source !== 'custom' && variant.source === 'custom') {
|
|
120
|
+
existing.source = 'custom';
|
|
121
|
+
}
|
|
122
|
+
if (!existing.fileUrl && variant.fileUrl) {
|
|
123
|
+
existing.fileUrl = variant.fileUrl;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const collectComponentTextVariants = (component, configuredLookup, output) => {
|
|
127
|
+
const textAppearance = component?.appearance?.text;
|
|
128
|
+
if (!textAppearance) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const family = normalizeFamily(textAppearance.fontFamily) ?? normalizeFamily(textAppearance.fontSource?.family);
|
|
132
|
+
if (!family) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const configMatch = configuredLookup.get(family.toLowerCase());
|
|
136
|
+
const source = (textAppearance.fontSource?.source ?? configMatch?.source ?? 'google');
|
|
137
|
+
const fileUrl = normalizeFamily(textAppearance.fontSource?.fileUrl) ?? configMatch?.fileUrl ?? undefined;
|
|
138
|
+
const weights = new Set();
|
|
139
|
+
const styleWeights = [
|
|
140
|
+
textAppearance.fontWeight,
|
|
141
|
+
textAppearance.activeWord?.fontWeight,
|
|
142
|
+
textAppearance.activeLine?.fontWeight
|
|
143
|
+
];
|
|
144
|
+
for (const styleWeight of styleWeights) {
|
|
145
|
+
const parsed = normalizeWeight(styleWeight);
|
|
146
|
+
if (parsed !== null) {
|
|
147
|
+
weights.add(parsed);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const variantWeight of parseWeightsFromVariants(textAppearance.fontSource?.variants)) {
|
|
151
|
+
weights.add(variantWeight);
|
|
152
|
+
}
|
|
153
|
+
if (weights.size === 0 && configMatch?.weights?.length) {
|
|
154
|
+
for (const configuredWeight of configMatch.weights) {
|
|
155
|
+
weights.add(configuredWeight);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (weights.size === 0) {
|
|
159
|
+
weights.add(DEFAULT_WEIGHT);
|
|
160
|
+
}
|
|
161
|
+
for (const weight of weights) {
|
|
162
|
+
upsertVariant(output, {
|
|
163
|
+
family,
|
|
164
|
+
weight,
|
|
165
|
+
source,
|
|
166
|
+
fileUrl
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
export const extractConfiguredFontVariants = (configuredFonts = []) => {
|
|
171
|
+
const variants = new Map();
|
|
172
|
+
const parsedFonts = parseConfiguredFonts(configuredFonts);
|
|
173
|
+
for (const parsedFont of parsedFonts) {
|
|
174
|
+
for (const weight of parsedFont.weights) {
|
|
175
|
+
upsertVariant(variants, {
|
|
176
|
+
family: parsedFont.family,
|
|
177
|
+
weight,
|
|
178
|
+
source: parsedFont.source,
|
|
179
|
+
fileUrl: parsedFont.fileUrl
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return [...variants.values()];
|
|
184
|
+
};
|
|
185
|
+
export const discoverRequiredFontVariants = (sceneData, configuredFonts = []) => {
|
|
186
|
+
const variants = new Map();
|
|
187
|
+
const parsedConfiguredFonts = parseConfiguredFonts(configuredFonts);
|
|
188
|
+
const configuredLookup = createConfiguredLookup(parsedConfiguredFonts);
|
|
189
|
+
for (const layer of sceneData.layers ?? []) {
|
|
190
|
+
for (const component of layer.components ?? []) {
|
|
191
|
+
if (component.type !== 'TEXT' && component.type !== 'SUBTITLES') {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
collectComponentTextVariants(component, configuredLookup, variants);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Preserve backwards compatibility with caller-supplied fonts by including explicit config entries.
|
|
198
|
+
for (const configuredVariant of extractConfiguredFontVariants(configuredFonts)) {
|
|
199
|
+
upsertVariant(variants, configuredVariant);
|
|
200
|
+
}
|
|
201
|
+
return [...variants.values()];
|
|
202
|
+
};
|
|
@@ -18,6 +18,7 @@ export declare class Layer implements ILayer {
|
|
|
18
18
|
});
|
|
19
19
|
build(): Promise<void>;
|
|
20
20
|
addComponent(component: IComponent): void;
|
|
21
|
+
syncDisplayObjects(): boolean;
|
|
21
22
|
removeComponent(component: IComponent): void;
|
|
22
23
|
update(props: Partial<SceneLayerInput>): void;
|
|
23
24
|
setOrder(order: number): void;
|
|
@@ -52,6 +52,36 @@ export class Layer {
|
|
|
52
52
|
this.components.sort((a, b) => a.props.timeline.startAt - b.props.timeline.startAt);
|
|
53
53
|
this.#emit('layerschange');
|
|
54
54
|
}
|
|
55
|
+
syncDisplayObjects() {
|
|
56
|
+
let changed = false;
|
|
57
|
+
if (!this.#displayObject) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
for (let index = 0; index < this.components.length; index += 1) {
|
|
61
|
+
const component = this.components[index];
|
|
62
|
+
const displayObject = component.displayObject;
|
|
63
|
+
if (!displayObject) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const parent = displayObject.parent;
|
|
67
|
+
if (parent !== this.#displayObject) {
|
|
68
|
+
if (parent && typeof parent.removeChild === 'function') {
|
|
69
|
+
parent.removeChild(displayObject);
|
|
70
|
+
}
|
|
71
|
+
this.#displayObject.addChild(displayObject);
|
|
72
|
+
changed = true;
|
|
73
|
+
}
|
|
74
|
+
if (typeof this.#displayObject.getChildIndex === 'function' &&
|
|
75
|
+
typeof this.#displayObject.setChildIndex === 'function') {
|
|
76
|
+
const currentIndex = this.#displayObject.getChildIndex(displayObject);
|
|
77
|
+
if (currentIndex !== index) {
|
|
78
|
+
this.#displayObject.setChildIndex(displayObject, index);
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return changed;
|
|
84
|
+
}
|
|
55
85
|
removeComponent(component) {
|
|
56
86
|
const hasComponent = this.components.find((c) => c.id === component.id);
|
|
57
87
|
if (hasComponent) {
|
|
@@ -3,11 +3,14 @@ import { EventManager } from './EventManager.js';
|
|
|
3
3
|
import type { ResourceManager, IComponent, ComponentData } from '..';
|
|
4
4
|
import type { AppearanceInput } from '..';
|
|
5
5
|
import { AppManager } from './AppManager.svelte.js';
|
|
6
|
+
import { LayersManager } from './LayersManager.svelte.js';
|
|
6
7
|
export declare class RenderManager {
|
|
8
|
+
#private;
|
|
7
9
|
private state;
|
|
8
10
|
private componentsManager;
|
|
9
11
|
private eventManager;
|
|
10
12
|
private appManager;
|
|
13
|
+
private layersManager;
|
|
11
14
|
private lastActiveById;
|
|
12
15
|
private lastRenderTime;
|
|
13
16
|
constructor(cradle: {
|
|
@@ -15,6 +18,7 @@ export declare class RenderManager {
|
|
|
15
18
|
componentsManager: ResourceManager<IComponent, ComponentData, AppearanceInput>;
|
|
16
19
|
eventManager: EventManager;
|
|
17
20
|
appManager: AppManager;
|
|
21
|
+
layersManager: LayersManager;
|
|
18
22
|
});
|
|
19
23
|
private initializeEventListeners;
|
|
20
24
|
private handleBeforeRender;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { StateManager } from './StateManager.svelte.js';
|
|
2
2
|
import { EventManager } from './EventManager.js';
|
|
3
3
|
import { AppManager } from './AppManager.svelte.js';
|
|
4
|
+
import { LayersManager } from './LayersManager.svelte.js';
|
|
4
5
|
export class RenderManager {
|
|
5
6
|
state;
|
|
6
7
|
componentsManager;
|
|
7
8
|
eventManager;
|
|
8
9
|
appManager;
|
|
10
|
+
layersManager;
|
|
9
11
|
// Track last visibility to ensure we run one more update to hide components that just became invisible
|
|
10
12
|
lastActiveById = new Map();
|
|
11
13
|
lastRenderTime = -1;
|
|
@@ -14,6 +16,7 @@ export class RenderManager {
|
|
|
14
16
|
this.componentsManager = cradle.componentsManager;
|
|
15
17
|
this.eventManager = cradle.eventManager;
|
|
16
18
|
this.appManager = cradle.appManager;
|
|
19
|
+
this.layersManager = cradle.layersManager;
|
|
17
20
|
this.initializeEventListeners();
|
|
18
21
|
}
|
|
19
22
|
initializeEventListeners() {
|
|
@@ -27,6 +30,18 @@ export class RenderManager {
|
|
|
27
30
|
});
|
|
28
31
|
}
|
|
29
32
|
async handleBeforeRender() { }
|
|
33
|
+
#syncLayerDisplayObjects() {
|
|
34
|
+
const layers = this.layersManager.getAll();
|
|
35
|
+
let changed = false;
|
|
36
|
+
for (const layer of layers) {
|
|
37
|
+
if (layer.syncDisplayObjects()) {
|
|
38
|
+
changed = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (changed) {
|
|
42
|
+
this.state.markDirty();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
30
45
|
async render() {
|
|
31
46
|
const components = this.componentsManager.getAll();
|
|
32
47
|
const currentTime = this.state.currentTime;
|
|
@@ -47,6 +62,7 @@ export class RenderManager {
|
|
|
47
62
|
for (const e of entries) {
|
|
48
63
|
this.lastActiveById.set(e.component.id, e.shouldBeVisible);
|
|
49
64
|
}
|
|
65
|
+
this.#syncLayerDisplayObjects();
|
|
50
66
|
this.appManager.render();
|
|
51
67
|
this.lastRenderTime = currentTime;
|
|
52
68
|
}
|
|
@@ -65,13 +65,21 @@ export type RenderFrameRangeItem = {
|
|
|
65
65
|
frameIndex: number;
|
|
66
66
|
frame: string | ArrayBuffer | Blob;
|
|
67
67
|
isDuplicate: boolean;
|
|
68
|
+
mimeType?: string;
|
|
68
69
|
release: () => void;
|
|
69
70
|
};
|
|
71
|
+
export type FrameImageFormat = 'jpg' | 'jpeg' | 'png';
|
|
72
|
+
export type FrameImageEncodingOptions = {
|
|
73
|
+
imageFormat?: FrameImageFormat;
|
|
74
|
+
imageQuality?: number;
|
|
75
|
+
};
|
|
70
76
|
export type RenderFrameRangeOptions = {
|
|
71
77
|
fromFrame: number;
|
|
72
78
|
toFrame: number;
|
|
73
79
|
format?: 'arraybuffer' | 'blob' | 'png' | 'jpg' | 'jpeg';
|
|
74
80
|
quality?: number;
|
|
81
|
+
imageFormat?: FrameImageFormat;
|
|
82
|
+
imageQuality?: number;
|
|
75
83
|
skipDuplicates?: boolean;
|
|
76
84
|
signal?: AbortSignal;
|
|
77
85
|
onFrame: (item: RenderFrameRangeItem) => Promise<void> | void;
|
|
@@ -92,3 +100,6 @@ export declare class DeterministicRenderError extends Error {
|
|
|
92
100
|
sceneTime: number;
|
|
93
101
|
});
|
|
94
102
|
}
|
|
103
|
+
export declare class RenderFrameEncodingError extends Error {
|
|
104
|
+
constructor(message: string);
|
|
105
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { BuilderState, StateEvents, TimelineEvents, RenderEvents, PlaybackEvents, LayerEvents, ComponentEvents, SubtitlesEvents, EventMap, EventType, EventPayload, ComponentRefreshType, HookType, SceneLayerComponentType, SplitScreenChunk, SplitScreen, } from './types.js';
|
|
2
2
|
export type { MediaComponent, ResourceManager, ComponentData, ComponentProps, PixiComponent, ResourceTypes, HookHandler, HookHandlers, } from './types.js';
|
|
3
|
-
export { DeterministicMediaConfigShape, defaultDeterministicMediaConfig, DeterministicRenderError } from './deterministic.js';
|
|
4
|
-
export type { DeterministicMediaConfig, DeterministicFrameRequest, DeterministicFramePayload, DeterministicFrameProvider, DeterministicFrameOverride, DeterministicDiagnosticsReport, RenderFrameRangeOptions, RenderFrameRangeItem, RenderFrameRangeSummary } from './deterministic.js';
|
|
3
|
+
export { DeterministicMediaConfigShape, defaultDeterministicMediaConfig, DeterministicRenderError, RenderFrameEncodingError } from './deterministic.js';
|
|
4
|
+
export type { DeterministicMediaConfig, DeterministicFrameRequest, DeterministicFramePayload, DeterministicFrameProvider, DeterministicFrameOverride, DeterministicDiagnosticsReport, FrameImageFormat, FrameImageEncodingOptions, RenderFrameRangeOptions, RenderFrameRangeItem, RenderFrameRangeSummary } from './deterministic.js';
|
|
5
5
|
export type { Component as IComponent, Layer as ILayer, SceneBuilder as ISceneBuilder, ComponentContext as IComponentContext, ComponentBuilder as IComponentBuilder, StateManager as IStateManager, ComponentHook as IComponentHook, ComponentBuildStrategy as IComponentBuildStrategy, } from './types.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Runtime types for the SceneBuilder application
|
|
2
2
|
// These represent runtime interfaces for class instances
|
|
3
3
|
// They are prefixed with "I" to distinguish from data schemas
|
|
4
|
-
export { DeterministicMediaConfigShape, defaultDeterministicMediaConfig, DeterministicRenderError } from './deterministic.js';
|
|
4
|
+
export { DeterministicMediaConfigShape, defaultDeterministicMediaConfig, DeterministicRenderError, RenderFrameEncodingError } from './deterministic.js';
|
|
@@ -5,7 +5,7 @@ import type { LayersManager } from '../../managers/LayersManager.svelte.js';
|
|
|
5
5
|
import type { SubtitlesManager } from '../../managers/SubtitlesManager.svelte.js';
|
|
6
6
|
import type { EventManager } from '../../managers/EventManager.js';
|
|
7
7
|
import type { Component as SceneLayerComponent, ComponentBase, AppearanceInput, Scene, RenderEnvironment, ComponentInput, SceneLayerInput, SceneLayer, VideoComponentShape, ImageComponentShape, GifComponentShape, Subtitle } from '../..';
|
|
8
|
-
import type { DeterministicMediaConfig, DeterministicFrameProvider, DeterministicDiagnosticsReport, RenderFrameRangeOptions, RenderFrameRangeSummary } from './deterministic.js';
|
|
8
|
+
import type { DeterministicMediaConfig, DeterministicFrameProvider, DeterministicDiagnosticsReport, FrameImageEncodingOptions, RenderFrameRangeOptions, RenderFrameRangeSummary } from './deterministic.js';
|
|
9
9
|
declare const SCENE_LAYER_COMPONENT_TYPE: readonly ["IMAGE", "GIF", "VIDEO", "TEXT", "SHAPE", "AUDIO", "COLOR", "GRADIENT", "SUBTITLES"];
|
|
10
10
|
export type SceneLayerComponentType = (typeof SCENE_LAYER_COMPONENT_TYPE)[number];
|
|
11
11
|
type MediaShape = z.infer<typeof VideoComponentShape>;
|
|
@@ -177,6 +177,7 @@ export interface Layer {
|
|
|
177
177
|
update(layerData: Partial<SceneLayerInput>): void;
|
|
178
178
|
addComponent(component: Component): void;
|
|
179
179
|
removeComponent(component: Component): void;
|
|
180
|
+
syncDisplayObjects(): boolean;
|
|
180
181
|
build(): Promise<void>;
|
|
181
182
|
setOrder(order: number): void;
|
|
182
183
|
getData(): SceneLayer;
|
|
@@ -263,10 +264,10 @@ export interface SceneBuilder {
|
|
|
263
264
|
getDeterministicFrameProvider(): DeterministicFrameProvider | null;
|
|
264
265
|
getDeterministicMediaConfig(): DeterministicMediaConfig;
|
|
265
266
|
getDiagnosticsReport(): DeterministicDiagnosticsReport | null;
|
|
266
|
-
seekAndRenderFrame(time: number, target?: DisplayObject | RenderTexture, format?: string, quality?: number): Promise<string | ArrayBuffer | Blob>;
|
|
267
|
+
seekAndRenderFrame(time: number, target?: DisplayObject | RenderTexture, format?: string, quality?: number, imageOptions?: FrameImageEncodingOptions): Promise<string | ArrayBuffer | Blob>;
|
|
267
268
|
renderFrameRange(options: RenderFrameRangeOptions): Promise<RenderFrameRangeSummary>;
|
|
268
269
|
isSceneDirty(time: number): Promise<boolean>;
|
|
269
|
-
renderFrame(target?: DisplayObject | RenderTexture, format?: string, quality?: number): Promise<string | ArrayBuffer | Blob>;
|
|
270
|
+
renderFrame(target?: DisplayObject | RenderTexture, format?: string, quality?: number, imageOptions?: FrameImageEncodingOptions): Promise<string | ArrayBuffer | Blob>;
|
|
270
271
|
log(message: string): void;
|
|
271
272
|
play(changeState?: boolean): void;
|
|
272
273
|
pause(changeState?: boolean): void;
|
package/dist/utils/document.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { FontType } from '..';
|
|
2
|
-
|
|
2
|
+
import type { FontVariantDescriptor } from '../fonts/fontDiscovery.js';
|
|
3
|
+
export declare const loadFonts: (fonts: FontType[], variants?: FontVariantDescriptor[]) => Promise<void>;
|
package/dist/utils/document.js
CHANGED
|
@@ -1,36 +1,125 @@
|
|
|
1
|
+
import { extractConfiguredFontVariants } from '../fonts/fontDiscovery.js';
|
|
2
|
+
const FONT_PROBE_TEXT = 'BESbswy';
|
|
3
|
+
const FONT_PROBE_SIZE = '60px';
|
|
4
|
+
const customFaceRegistry = new Set();
|
|
5
|
+
const isFontApiAvailable = () => typeof document !== 'undefined' &&
|
|
6
|
+
typeof document.fonts !== 'undefined' &&
|
|
7
|
+
typeof document.createElement === 'function';
|
|
8
|
+
const loadStylesheet = async (href) => {
|
|
9
|
+
await new Promise((resolve) => {
|
|
10
|
+
const link = document.createElement('link');
|
|
11
|
+
link.href = href;
|
|
12
|
+
link.rel = 'stylesheet';
|
|
13
|
+
link.onload = () => resolve();
|
|
14
|
+
link.onerror = () => {
|
|
15
|
+
console.warn(`Failed to load font stylesheet: ${href}`);
|
|
16
|
+
resolve();
|
|
17
|
+
};
|
|
18
|
+
document.head.appendChild(link);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const groupByFamily = (variants) => {
|
|
22
|
+
const grouped = new Map();
|
|
23
|
+
for (const variant of variants) {
|
|
24
|
+
if (!grouped.has(variant.family)) {
|
|
25
|
+
grouped.set(variant.family, new Set());
|
|
26
|
+
}
|
|
27
|
+
grouped.get(variant.family).add(variant.weight);
|
|
28
|
+
}
|
|
29
|
+
return grouped;
|
|
30
|
+
};
|
|
31
|
+
const buildGoogleCss2Url = (variants) => {
|
|
32
|
+
if (!variants.length) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const groupedFamilies = groupByFamily(variants);
|
|
36
|
+
const familyQueryParts = [];
|
|
37
|
+
for (const [family, weights] of groupedFamilies.entries()) {
|
|
38
|
+
const encodedFamily = encodeURIComponent(family).replace(/%20/g, '+');
|
|
39
|
+
const sortedWeights = [...weights].sort((a, b) => a - b);
|
|
40
|
+
const familyPart = sortedWeights.length
|
|
41
|
+
? `family=${encodedFamily}:wght@${sortedWeights.join(';')}`
|
|
42
|
+
: `family=${encodedFamily}`;
|
|
43
|
+
familyQueryParts.push(familyPart);
|
|
44
|
+
}
|
|
45
|
+
if (!familyQueryParts.length) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return `https://fonts.googleapis.com/css2?${familyQueryParts.join('&')}&display=swap`;
|
|
49
|
+
};
|
|
50
|
+
const registerCustomFontFaces = async (variants) => {
|
|
51
|
+
const customVariants = variants.filter((variant) => variant.source === 'custom');
|
|
52
|
+
if (!customVariants.length) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const hasFontFaceApi = typeof FontFace !== 'undefined';
|
|
56
|
+
for (const variant of customVariants) {
|
|
57
|
+
if (!variant.fileUrl) {
|
|
58
|
+
console.warn(`Custom font "${variant.family}" (weight ${variant.weight}) is missing fileUrl/src and cannot be preloaded.`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!hasFontFaceApi) {
|
|
62
|
+
console.warn(`FontFace API is unavailable. Skipping custom font preload for "${variant.family}" (${variant.weight}).`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const registryKey = `${variant.family}::${variant.weight}::${variant.fileUrl}`;
|
|
66
|
+
if (customFaceRegistry.has(registryKey)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const face = new FontFace(variant.family, `url("${variant.fileUrl}")`, {
|
|
71
|
+
weight: String(variant.weight),
|
|
72
|
+
style: 'normal'
|
|
73
|
+
});
|
|
74
|
+
await face.load();
|
|
75
|
+
document.fonts.add(face);
|
|
76
|
+
customFaceRegistry.add(registryKey);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.warn(`Failed to preload custom font "${variant.family}" (${variant.weight}) from ${variant.fileUrl}.`, error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const loadDescriptors = async (variants) => {
|
|
84
|
+
const descriptorPromises = variants.map(async (variant) => {
|
|
85
|
+
const descriptor = `${variant.weight} ${FONT_PROBE_SIZE} "${variant.family}"`;
|
|
86
|
+
try {
|
|
87
|
+
const loadedFaces = await document.fonts.load(descriptor, FONT_PROBE_TEXT);
|
|
88
|
+
if (!loadedFaces.length) {
|
|
89
|
+
console.warn(`Font descriptor did not resolve any faces: ${descriptor}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.warn(`Failed to load font descriptor: ${descriptor}`, error);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
await Promise.all(descriptorPromises);
|
|
97
|
+
};
|
|
1
98
|
async function waitForAllFonts() {
|
|
2
|
-
|
|
3
|
-
if (typeof document === 'undefined' || !document.fonts) {
|
|
99
|
+
if (!isFontApiAvailable()) {
|
|
4
100
|
return;
|
|
5
101
|
}
|
|
6
102
|
try {
|
|
7
103
|
await document.fonts.ready;
|
|
8
104
|
}
|
|
9
105
|
catch (error) {
|
|
10
|
-
console.warn('One or more fonts failed to
|
|
106
|
+
console.warn('One or more fonts failed to reach ready state.', error);
|
|
11
107
|
}
|
|
12
108
|
}
|
|
13
|
-
export const loadFonts = async function (fonts) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
link.rel = 'stylesheet';
|
|
26
|
-
link.onload = () => resolve(true);
|
|
27
|
-
link.onerror = () => {
|
|
28
|
-
console.warn(`Failed to load font: ${font}`);
|
|
29
|
-
resolve(false);
|
|
30
|
-
};
|
|
31
|
-
document.head.appendChild(link);
|
|
32
|
-
}));
|
|
33
|
-
await Promise.all(fontPromises);
|
|
109
|
+
export const loadFonts = async function (fonts, variants) {
|
|
110
|
+
if (!isFontApiAvailable()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const resolvedVariants = variants && variants.length ? variants : extractConfiguredFontVariants(fonts);
|
|
114
|
+
if (!resolvedVariants.length) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const googleVariants = resolvedVariants.filter((variant) => variant.source === 'google');
|
|
118
|
+
const googleCss2Url = buildGoogleCss2Url(googleVariants);
|
|
119
|
+
if (googleCss2Url) {
|
|
120
|
+
await loadStylesheet(googleCss2Url);
|
|
34
121
|
}
|
|
122
|
+
await registerCustomFontFaces(resolvedVariants);
|
|
123
|
+
await loadDescriptors(resolvedVariants);
|
|
35
124
|
await waitForAllFonts();
|
|
36
125
|
};
|