visualfries 0.1.10115 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -0
- package/bin/visualfries.js +1570 -0
- package/dist/DIContainer.d.ts +2 -1
- package/dist/DIContainer.js +16 -5
- package/dist/SceneBuilder.svelte.d.ts +70 -1
- package/dist/SceneBuilder.svelte.js +96 -29
- package/dist/agent/audioMixer.d.ts +34 -0
- package/dist/agent/audioMixer.js +308 -0
- package/dist/agent/broll.d.ts +35 -0
- package/dist/agent/broll.js +226 -0
- package/dist/agent/capabilities.d.ts +34 -0
- package/dist/agent/capabilities.js +246 -0
- package/dist/agent/captionScene.d.ts +23 -0
- package/dist/agent/captionScene.js +204 -0
- package/dist/agent/catalog.d.ts +24 -0
- package/dist/agent/catalog.js +67 -0
- package/dist/agent/cueFiles.d.ts +54 -0
- package/dist/agent/cueFiles.js +219 -0
- package/dist/agent/cuePresets.d.ts +7 -0
- package/dist/agent/cuePresets.js +50 -0
- package/dist/agent/deterministicMedia.d.ts +61 -0
- package/dist/agent/deterministicMedia.js +303 -0
- package/dist/agent/index.d.ts +18 -0
- package/dist/agent/index.js +18 -0
- package/dist/agent/inspectScene.d.ts +36 -0
- package/dist/agent/inspectScene.js +139 -0
- package/dist/agent/localRender.d.ts +82 -0
- package/dist/agent/localRender.js +702 -0
- package/dist/agent/overlays.d.ts +40 -0
- package/dist/agent/overlays.js +325 -0
- package/dist/agent/productionPlan.d.ts +131 -0
- package/dist/agent/productionPlan.js +322 -0
- package/dist/agent/renderPlan.d.ts +27 -0
- package/dist/agent/renderPlan.js +68 -0
- package/dist/agent/renderRanges.d.ts +24 -0
- package/dist/agent/renderRanges.js +73 -0
- package/dist/agent/runtimeSupport.d.ts +19 -0
- package/dist/agent/runtimeSupport.js +192 -0
- package/dist/agent/streamingEncoder.d.ts +31 -0
- package/dist/agent/streamingEncoder.js +208 -0
- package/dist/agent/transcripts.d.ts +29 -0
- package/dist/agent/transcripts.js +146 -0
- package/dist/agent/transitions.d.ts +20 -0
- package/dist/agent/transitions.js +169 -0
- package/dist/animations/PixiAnimationTarget.d.ts +29 -0
- package/dist/animations/PixiAnimationTarget.js +63 -0
- package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
- package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
- package/dist/browser/browserExport.d.ts +73 -0
- package/dist/browser/browserExport.js +180 -0
- package/dist/browser/glTransitions.d.ts +37 -0
- package/dist/browser/glTransitions.js +261 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/mediaDecode.d.ts +31 -0
- package/dist/browser/mediaDecode.js +152 -0
- package/dist/builders/PixiComponentBuilder.d.ts +6 -0
- package/dist/builders/PixiComponentBuilder.js +12 -0
- package/dist/builders/_ComponentState.svelte.d.ts +7 -5
- package/dist/builders/_ComponentState.svelte.js +29 -15
- package/dist/builders/html/StyleBuilder.js +9 -8
- package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
- package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
- package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
- package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
- package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
- package/dist/commands/SeekCommand.js +19 -6
- package/dist/components/AnimatedGIF.d.ts +3 -0
- package/dist/components/AnimatedGIF.js +28 -1
- package/dist/components/Component.svelte.d.ts +5 -5
- package/dist/components/Component.svelte.js +29 -13
- package/dist/components/ComponentContext.svelte.js +68 -23
- package/dist/components/hooks/AnimationHook.js +1 -1
- package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
- package/dist/components/hooks/CanvasFillHook.js +75 -0
- package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
- package/dist/components/hooks/HtmlTextHook.js +11 -3
- package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
- package/dist/components/hooks/MediaHook.js +172 -47
- package/dist/components/hooks/MediaSeekingHook.js +138 -88
- package/dist/components/hooks/PixiGifHook.js +4 -4
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.d.ts +3 -0
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +85 -8
- package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
- package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
- package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
- package/dist/components/hooks/SubtitlesHook.js +48 -24
- package/dist/components/hooks/VerifyMediaHook.js +2 -2
- package/dist/directors/ComponentDirector.d.ts +1 -0
- package/dist/directors/ComponentDirector.js +41 -8
- package/dist/factories/SceneBuilderFactory.d.ts +3 -0
- package/dist/factories/SceneBuilderFactory.js +6 -0
- package/dist/fonts/fontDiscovery.js +12 -1
- package/dist/layers/Layer.svelte.d.ts +54 -0
- package/dist/layers/Layer.svelte.js +2 -25
- package/dist/managers/AppManager.svelte.d.ts +12 -0
- package/dist/managers/AppManager.svelte.js +180 -8
- package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
- package/dist/managers/ComponentsManager.svelte.js +29 -15
- package/dist/managers/DeterministicMediaManager.d.ts +6 -0
- package/dist/managers/DeterministicMediaManager.js +57 -3
- package/dist/managers/EventManager.js +3 -1
- package/dist/managers/LayersManager.svelte.d.ts +54 -0
- package/dist/managers/RenderManager.d.ts +5 -0
- package/dist/managers/RenderManager.js +67 -21
- package/dist/managers/StateManager.svelte.d.ts +56 -0
- package/dist/managers/StateManager.svelte.js +60 -58
- package/dist/managers/SubtitlesManager.svelte.js +1 -3
- package/dist/managers/TimeManager.svelte.d.ts +4 -0
- package/dist/managers/TimeManager.svelte.js +23 -7
- package/dist/schemas/runtime/deterministic.d.ts +3 -0
- package/dist/schemas/runtime/deterministic.js +5 -1
- package/dist/schemas/runtime/types.d.ts +36 -15
- package/dist/schemas/scene/components.d.ts +126 -0
- package/dist/schemas/scene/components.js +11 -0
- package/dist/schemas/scene/core.d.ts +109 -10
- package/dist/schemas/scene/core.js +1 -1
- package/dist/utils/mediaWindow.d.ts +13 -0
- package/dist/utils/mediaWindow.js +25 -0
- package/dist/utils/svgGenerator.js +1 -1
- package/dist/utils/timelineWindow.d.ts +2 -0
- package/dist/utils/timelineWindow.js +7 -0
- package/dist/utils/utils.js +2 -4
- package/docs/AGENT_PATTERNS.md +243 -0
- package/docs/AGENT_WORKFLOW.md +364 -0
- package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
- package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
- package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
- package/docs/DETERMINISTIC_RENDERER.md +83 -0
- package/docs/NEXT_RELEASE.md +28 -0
- package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
- package/docs/examples/agent-cues.json +49 -0
- package/docs/examples/agent-transcript.json +23 -0
- package/docs/examples/agent-transcript.srt +7 -0
- package/docs/examples/agent-transcript.vtt +7 -0
- package/docs/examples/image-animation.json +87 -0
- package/docs/examples/native-text-metric-badge.json +109 -0
- package/package.json +133 -100
- package/skills/visualfries/SKILL.md +328 -0
|
@@ -47,23 +47,24 @@ export class StyleBuilder {
|
|
|
47
47
|
const textAppearance = appearance?.text;
|
|
48
48
|
if (textAppearance?.shadow) {
|
|
49
49
|
component.effects.map['textShadow'] = {
|
|
50
|
-
|
|
50
|
+
structured: true,
|
|
51
51
|
enabled: textAppearance.shadow.enabled ? true : false,
|
|
52
|
-
color:
|
|
53
|
-
? textAppearance.color
|
|
54
|
-
: textAppearance.color.colors[0],
|
|
52
|
+
color: textAppearance.shadow.color ?? '#000000',
|
|
55
53
|
size: textAppearance.shadow.size,
|
|
56
|
-
blur: textAppearance.shadow.blur,
|
|
54
|
+
blur: textAppearance.shadow.blur ?? 0,
|
|
55
|
+
offsetX: textAppearance.shadow.offsetX ?? 0,
|
|
56
|
+
offsetY: textAppearance.shadow.offsetY ?? 0,
|
|
57
|
+
opacity: textAppearance.shadow.opacity ?? 1,
|
|
57
58
|
type: 'textShadow'
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
if (textAppearance?.outline) {
|
|
61
|
-
// TODO
|
|
62
62
|
component.effects.map['textOutline'] = {
|
|
63
|
-
|
|
63
|
+
structured: true,
|
|
64
64
|
enabled: textAppearance.outline.enabled ? true : false,
|
|
65
65
|
size: textAppearance.outline.size,
|
|
66
|
-
color: textAppearance.outline.color
|
|
66
|
+
color: textAppearance.outline.color,
|
|
67
|
+
opacity: textAppearance.outline.opacity ?? 1
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
return component;
|
|
@@ -23,7 +23,8 @@ export class TextComponentHtmlBuilder {
|
|
|
23
23
|
'height',
|
|
24
24
|
'justifyContent',
|
|
25
25
|
'alignItems',
|
|
26
|
-
'display'
|
|
26
|
+
'display',
|
|
27
|
+
'transform'
|
|
27
28
|
];
|
|
28
29
|
if (get(appearance, 'background.enabled', false) &&
|
|
29
30
|
get(appearance, 'background.target') === 'wrapper') {
|
|
@@ -50,7 +51,8 @@ export class TextComponentHtmlBuilder {
|
|
|
50
51
|
'alignItems',
|
|
51
52
|
'display',
|
|
52
53
|
'width',
|
|
53
|
-
'height'
|
|
54
|
+
'height',
|
|
55
|
+
'transform'
|
|
54
56
|
];
|
|
55
57
|
let addPadding = false;
|
|
56
58
|
if (get(appearance, 'background.enabled', false)) {
|
|
@@ -48,7 +48,7 @@ export class AppearanceStyleProcessor {
|
|
|
48
48
|
if (appearance.background && get(appearance.background, 'enabled')) {
|
|
49
49
|
const color = ColorTransformer.transform(get(appearance.background, 'color', '#000000'), 'background');
|
|
50
50
|
if (get(appearance, 'background.radius', 0) > 0) {
|
|
51
|
-
styles.borderRadius = `${get(appearance.background, 'radius')}
|
|
51
|
+
styles.borderRadius = `${get(appearance.background, 'radius')}px`;
|
|
52
52
|
}
|
|
53
53
|
styles = { ...styles, ...color };
|
|
54
54
|
}
|
|
@@ -32,6 +32,13 @@ export class TextAppearanceStyleProcessor {
|
|
|
32
32
|
if (textAppearance.textTransform && textAppearance.textTransform !== 'none') {
|
|
33
33
|
styles.textTransform = textAppearance.textTransform;
|
|
34
34
|
}
|
|
35
|
+
if (textAppearance.padding !== undefined) {
|
|
36
|
+
styles.padding =
|
|
37
|
+
typeof textAppearance.padding === 'number'
|
|
38
|
+
? `${textAppearance.padding}px`
|
|
39
|
+
: `${textAppearance.padding.top}px ${textAppearance.padding.right}px ${textAppearance.padding.bottom}px ${textAppearance.padding.left}px`;
|
|
40
|
+
styles.boxSizing = 'border-box';
|
|
41
|
+
}
|
|
35
42
|
return styles;
|
|
36
43
|
}
|
|
37
44
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TextEffectPresetName, TextShadowBuilder } from '../TextShadowBuilder.js';
|
|
3
|
+
import tinycolor from 'tinycolor2';
|
|
3
4
|
const GenericTextEffectShape = z.object({
|
|
4
5
|
enabled: z.boolean().optional(),
|
|
5
6
|
preset: z.enum(TextEffectPresetName).optional(),
|
|
6
7
|
size: z.number().optional(),
|
|
7
8
|
color: z.string().optional(),
|
|
8
|
-
opacity: z.number().optional()
|
|
9
|
+
opacity: z.number().optional(),
|
|
10
|
+
blur: z.number().optional(),
|
|
11
|
+
offsetX: z.number().optional(),
|
|
12
|
+
offsetY: z.number().optional(),
|
|
13
|
+
structured: z.boolean().optional()
|
|
9
14
|
});
|
|
10
15
|
export class TextEffectsStyleProcessor {
|
|
11
16
|
process(effectsMap) {
|
|
@@ -24,24 +29,38 @@ export class TextEffectsStyleProcessor {
|
|
|
24
29
|
const data = parsedEffect.data;
|
|
25
30
|
const size = data.size || 0.3;
|
|
26
31
|
const color = data.color || '#000000';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if (data.structured) {
|
|
33
|
+
const strokeColor = data.opacity === undefined || data.opacity === 1
|
|
34
|
+
? color
|
|
35
|
+
: tinycolor(color).setAlpha(data.opacity).toRgbString();
|
|
36
|
+
styles.webkitTextStroke = `${size}px ${strokeColor}`;
|
|
37
|
+
styles.paintOrder = 'stroke fill';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const shadow = TextShadowBuilder.build({
|
|
41
|
+
preset: TextEffectPresetName.OUTLINE,
|
|
42
|
+
size,
|
|
43
|
+
color,
|
|
44
|
+
opacity: data.opacity ?? 1
|
|
45
|
+
});
|
|
46
|
+
if (shadow && shadow !== 'none') {
|
|
47
|
+
shadowStrings.push(shadow);
|
|
48
|
+
}
|
|
35
49
|
}
|
|
36
|
-
// styles.webkitTextStroke = `${size}em ${color}`;
|
|
37
|
-
// styles.webkitTextFillColor = 'white';
|
|
38
|
-
// styles.paintOrder = 'stroke fill';
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
52
|
for (const effectName in effectsMap) {
|
|
42
53
|
const effect = effectsMap[effectName];
|
|
43
54
|
if (effect.type === 'textShadow') {
|
|
44
55
|
const parsedEffect = GenericTextEffectShape.safeParse(effect);
|
|
56
|
+
if (parsedEffect.success && parsedEffect.data.enabled && parsedEffect.data.structured) {
|
|
57
|
+
const data = parsedEffect.data;
|
|
58
|
+
const color = tinycolor(data.color ?? '#000000')
|
|
59
|
+
.setAlpha(data.opacity ?? 1)
|
|
60
|
+
.toRgbString();
|
|
61
|
+
shadowStrings.push(`${data.offsetX ?? 0}px ${data.offsetY ?? 0}px ${data.blur ?? data.size ?? 0}px ${color}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
45
64
|
if (parsedEffect.success &&
|
|
46
65
|
parsedEffect.data.enabled &&
|
|
47
66
|
parsedEffect.data.size &&
|
|
@@ -52,7 +71,7 @@ export class TextEffectsStyleProcessor {
|
|
|
52
71
|
preset,
|
|
53
72
|
size: data.size || 0.3,
|
|
54
73
|
color: data.color || '#000000',
|
|
55
|
-
opacity: data.opacity
|
|
74
|
+
opacity: data.opacity ?? 1
|
|
56
75
|
});
|
|
57
76
|
if (shadow && shadow !== 'none') {
|
|
58
77
|
shadowStrings.push(shadow);
|
|
@@ -27,7 +27,7 @@ export class ReplaceSourceOnTimeCommand {
|
|
|
27
27
|
if (!blob) {
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
const cacheKey = `replace:${componentId}:${frameIndex}:${base64data
|
|
30
|
+
const cacheKey = `replace:${componentId}:${frameIndex}:${this.#hashBase64(base64data)}`;
|
|
31
31
|
this.deterministicMediaManager.setOneTimeOverride(componentId, frameIndex, {
|
|
32
32
|
kind: 'blob',
|
|
33
33
|
cacheKey,
|
|
@@ -52,4 +52,12 @@ export class ReplaceSourceOnTimeCommand {
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
#hashBase64(value) {
|
|
56
|
+
let hash = 0x811c9dc5;
|
|
57
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
58
|
+
hash ^= value.charCodeAt(index);
|
|
59
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
60
|
+
}
|
|
61
|
+
return hash.toString(16).padStart(8, '0');
|
|
62
|
+
}
|
|
55
63
|
}
|
|
@@ -54,7 +54,8 @@ export class SeekCommand {
|
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
56
|
if (this.#hasBlurEffect(component)) {
|
|
57
|
-
const sourceElement = component.context.getResource('videoElement') ||
|
|
57
|
+
const sourceElement = component.context.getResource('videoElement') ||
|
|
58
|
+
component.context.getResource('imageElement');
|
|
58
59
|
if (!sourceElement) {
|
|
59
60
|
pending.push(component.id);
|
|
60
61
|
}
|
|
@@ -65,7 +66,7 @@ export class SeekCommand {
|
|
|
65
66
|
#getCurrentSceneFrameIndex() {
|
|
66
67
|
const fps = this.state.data?.settings?.fps || 30;
|
|
67
68
|
const currentTime = this.state.currentTime ?? 0;
|
|
68
|
-
return Math.max(0, Math.
|
|
69
|
+
return Math.max(0, Math.floor(currentTime * fps));
|
|
69
70
|
}
|
|
70
71
|
async #delay(ms) {
|
|
71
72
|
if (ms <= 0) {
|
|
@@ -119,7 +120,11 @@ export class SeekCommand {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
if (pending.length > 0) {
|
|
122
|
-
|
|
123
|
+
const message = `Deterministic media was not ready after seek for active components: ${pending.join(', ')}`;
|
|
124
|
+
if (this.deterministicMediaManager.config.strict) {
|
|
125
|
+
throw new Error(message);
|
|
126
|
+
}
|
|
127
|
+
console.warn(message);
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
130
|
async execute(args) {
|
|
@@ -129,6 +134,16 @@ export class SeekCommand {
|
|
|
129
134
|
}
|
|
130
135
|
const time = Math.max(0, Math.min(check.data.time, this.state.duration));
|
|
131
136
|
this.timeline.seek(time);
|
|
137
|
+
// Timeline events start rendering asynchronously. Await the shared render queue
|
|
138
|
+
// on clients so seek() is a frame-readiness boundary for preview captures.
|
|
139
|
+
if (this.state.environment !== 'server') {
|
|
140
|
+
await this.renderManager.render();
|
|
141
|
+
// Components may attach animation timelines during their first active update.
|
|
142
|
+
// Re-seek after construction, then render the authored state before resolving.
|
|
143
|
+
this.timeline.seek(time);
|
|
144
|
+
await this.renderManager.render();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
132
147
|
// Ensure a deterministic render on server after seek to advance media frames
|
|
133
148
|
if (this.state.environment === 'server') {
|
|
134
149
|
const deterministicEnabled = this.deterministicMediaManager.isEnabled();
|
|
@@ -152,12 +167,10 @@ export class SeekCommand {
|
|
|
152
167
|
// This fixes the race condition where subtitle animations are added
|
|
153
168
|
// AFTER the initial seek, causing them to miss their initial state.
|
|
154
169
|
this.timeline.seek(time);
|
|
170
|
+
await this.renderManager.render();
|
|
155
171
|
if (deterministicEnabled) {
|
|
156
172
|
await this.#renderUntilDeterministicReady();
|
|
157
173
|
}
|
|
158
|
-
else {
|
|
159
|
-
await this.renderManager.render();
|
|
160
|
-
}
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
}
|
|
@@ -9,6 +9,7 @@ interface FrameObject {
|
|
|
9
9
|
/** The end of the current frame, in milliseconds */
|
|
10
10
|
end: number;
|
|
11
11
|
}
|
|
12
|
+
export declare const resolveGifFrameIndexAtTime: (frames: ReadonlyArray<Pick<FrameObject, "start" | "end">>, duration: number, timeMs: number, loop: boolean) => number;
|
|
12
13
|
/** Default options for all AnimatedGIF objects. */
|
|
13
14
|
interface AnimatedGIFOptions {
|
|
14
15
|
/** Whether to start playing right away */
|
|
@@ -138,6 +139,8 @@ declare class AnimatedGIF extends Sprite {
|
|
|
138
139
|
constructor(frames: FrameObject[], options: Partial<AnimatedGIFOptions> & AnimatedGIFSize);
|
|
139
140
|
/** Stops the animation. */
|
|
140
141
|
stop(): void;
|
|
142
|
+
/** Seek using authored GIF frame delays rather than assuming a fixed FPS. */
|
|
143
|
+
seek(timeMs: number): void;
|
|
141
144
|
/** Plays the animation. */
|
|
142
145
|
play(): void;
|
|
143
146
|
/**
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { Sprite } from "pixi.js-legacy";
|
|
2
2
|
import { Texture, Renderer, settings, SCALE_MODES, Ticker, UPDATE_PRIORITY, } from "pixi.js-legacy";
|
|
3
3
|
import { parseGIF, decompressFrames } from "gifuct-js";
|
|
4
|
+
export const resolveGifFrameIndexAtTime = (frames, duration, timeMs, loop) => {
|
|
5
|
+
if (!frames.length || duration <= 0)
|
|
6
|
+
return 0;
|
|
7
|
+
const safeTime = Number.isFinite(timeMs) ? Math.max(0, timeMs) : 0;
|
|
8
|
+
const localTime = loop
|
|
9
|
+
? safeTime % duration
|
|
10
|
+
: Math.min(safeTime, Math.max(0, duration - Number.EPSILON));
|
|
11
|
+
const index = frames.findIndex((frame) => frame.start <= localTime && frame.end > localTime);
|
|
12
|
+
return index >= 0 ? index : frames.length - 1;
|
|
13
|
+
};
|
|
4
14
|
/**
|
|
5
15
|
* Runtime object to play animated GIFs. This object is similar to an AnimatedSprite.
|
|
6
16
|
* It support playback (seek, play, stop) as well as animation speed and looping.
|
|
@@ -206,6 +216,21 @@ class AnimatedGIF extends Sprite {
|
|
|
206
216
|
this._isConnectedToTicker = false;
|
|
207
217
|
}
|
|
208
218
|
}
|
|
219
|
+
/** Seek using authored GIF frame delays rather than assuming a fixed FPS. */
|
|
220
|
+
seek(timeMs) {
|
|
221
|
+
if (!Number.isFinite(this.duration) || this.duration <= 0) {
|
|
222
|
+
this._currentTime = 0;
|
|
223
|
+
if (this._frames.length > 0)
|
|
224
|
+
this.updateFrameIndex(0);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const safeTime = Number.isFinite(timeMs) ? Math.max(0, timeMs) : 0;
|
|
228
|
+
const localTime = this.loop
|
|
229
|
+
? safeTime % this.duration
|
|
230
|
+
: Math.min(safeTime, Math.max(0, this.duration - Number.EPSILON));
|
|
231
|
+
this._currentTime = localTime;
|
|
232
|
+
this.updateFrameIndex(resolveGifFrameIndexAtTime(this._frames, this.duration, safeTime, this.loop));
|
|
233
|
+
}
|
|
209
234
|
/** Plays the animation. */
|
|
210
235
|
play() {
|
|
211
236
|
if (this._playing) {
|
|
@@ -226,7 +251,9 @@ class AnimatedGIF extends Sprite {
|
|
|
226
251
|
* @readonly
|
|
227
252
|
*/
|
|
228
253
|
get progress() {
|
|
229
|
-
return this.
|
|
254
|
+
return Number.isFinite(this.duration) && this.duration > 0
|
|
255
|
+
? this._currentTime / this.duration
|
|
256
|
+
: 0;
|
|
230
257
|
}
|
|
231
258
|
/** `true` if the current animation is playing */
|
|
232
259
|
get playing() {
|
|
@@ -21,13 +21,13 @@ export declare class Component implements IComponent {
|
|
|
21
21
|
update(): Promise<void>;
|
|
22
22
|
refresh(type?: ComponentRefreshType): Promise<void>;
|
|
23
23
|
destroy(): Promise<void>;
|
|
24
|
-
updateAppearance(appearance: Partial<AppearanceInput>):
|
|
24
|
+
updateAppearance(appearance: Partial<AppearanceInput>): Component;
|
|
25
25
|
setStart(start: number): Component;
|
|
26
26
|
setEnd(end: number): Component;
|
|
27
|
-
updateText(text: string):
|
|
28
|
-
setText(text: string):
|
|
29
|
-
setVisible(visible: boolean):
|
|
30
|
-
setOrder(order: number):
|
|
27
|
+
updateText(text: string): Component;
|
|
28
|
+
setText(text: string): Component;
|
|
29
|
+
setVisible(visible: boolean): Component;
|
|
30
|
+
setOrder(order: number): Component;
|
|
31
31
|
onChange(callback: (changes: ComponentData) => void): () => void;
|
|
32
32
|
onTimelineChange(callback: (time: number) => void): () => void;
|
|
33
33
|
}
|
|
@@ -4,6 +4,7 @@ export class Component {
|
|
|
4
4
|
#context;
|
|
5
5
|
#hooks = [];
|
|
6
6
|
#autoRefresh = false;
|
|
7
|
+
#hookLock = Promise.resolve();
|
|
7
8
|
#eventUnsubscribers = [];
|
|
8
9
|
constructor(cradle) {
|
|
9
10
|
this.#properties = cradle.componentState;
|
|
@@ -47,12 +48,28 @@ export class Component {
|
|
|
47
48
|
}
|
|
48
49
|
addHook(hook, priority) {
|
|
49
50
|
const maxPriority = this.#hooks.length > 0 ? Math.max(...this.#hooks.map((h) => h.priority ?? 0)) : 0;
|
|
50
|
-
hook.priority = priority
|
|
51
|
+
hook.priority = priority ?? maxPriority + 1;
|
|
51
52
|
this.#hooks.push(hook);
|
|
53
|
+
this.#hooks.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
52
54
|
}
|
|
53
55
|
async #handle(type) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
// Serialize all hook executions via a promise-based mutex.
|
|
57
|
+
// Each call waits for the previous one to finish before running,
|
|
58
|
+
// so hooks on the same component never interleave.
|
|
59
|
+
let release;
|
|
60
|
+
const nextLock = new Promise((r) => {
|
|
61
|
+
release = r;
|
|
62
|
+
});
|
|
63
|
+
const prevLock = this.#hookLock;
|
|
64
|
+
this.#hookLock = nextLock;
|
|
65
|
+
await prevLock;
|
|
66
|
+
try {
|
|
67
|
+
const hooks = this.#hooks.filter((hook) => hook.types.includes(type));
|
|
68
|
+
await this.#context.runHooks(hooks, type);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
release();
|
|
72
|
+
}
|
|
56
73
|
}
|
|
57
74
|
async setup() {
|
|
58
75
|
this.#context.setComponentProps(this.#properties);
|
|
@@ -64,7 +81,6 @@ export class Component {
|
|
|
64
81
|
async refresh(type = 'refresh') {
|
|
65
82
|
this.#context.setComponentProps(this.#properties);
|
|
66
83
|
await this.#handle(type);
|
|
67
|
-
await this.update(); // also auto update to fix timing bug
|
|
68
84
|
}
|
|
69
85
|
async destroy() {
|
|
70
86
|
// Clean up all event listeners automatically
|
|
@@ -78,8 +94,8 @@ export class Component {
|
|
|
78
94
|
}
|
|
79
95
|
}
|
|
80
96
|
// Fluent method chaining - modify existing methods to return Component instance
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
updateAppearance(appearance) {
|
|
98
|
+
this.#properties.updateAppearance(appearance);
|
|
83
99
|
return this;
|
|
84
100
|
}
|
|
85
101
|
setStart(start) {
|
|
@@ -90,20 +106,20 @@ export class Component {
|
|
|
90
106
|
this.#properties.setEnd(end);
|
|
91
107
|
return this;
|
|
92
108
|
}
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
updateText(text) {
|
|
110
|
+
this.#properties.updateText(text);
|
|
95
111
|
return this;
|
|
96
112
|
}
|
|
97
113
|
// Simple fluent wrapper methods
|
|
98
|
-
|
|
114
|
+
setText(text) {
|
|
99
115
|
return this.updateText(text);
|
|
100
116
|
}
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
setVisible(visible) {
|
|
118
|
+
this.#properties.setVisible(visible);
|
|
103
119
|
return this;
|
|
104
120
|
}
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
setOrder(order) {
|
|
122
|
+
this.#properties.setOrder(order);
|
|
107
123
|
return this;
|
|
108
124
|
}
|
|
109
125
|
// Component-scoped event filtering methods
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { StateManager } from '../managers/StateManager.svelte.js';
|
|
2
2
|
import { EventManager } from '../managers/EventManager.js';
|
|
3
|
+
import { DeterministicRenderError } from '../schemas/runtime/deterministic.js';
|
|
4
|
+
import { isTimeWithinTimeline, TIMELINE_BOUNDARY_EPSILON } from '../utils/timelineWindow.js';
|
|
3
5
|
export class ComponentContext {
|
|
4
6
|
#data;
|
|
5
7
|
// context data replacement, useful for modifiing context of subtitles
|
|
@@ -16,6 +18,37 @@ export class ComponentContext {
|
|
|
16
18
|
this.#data = props;
|
|
17
19
|
this.#contextData = undefined;
|
|
18
20
|
}
|
|
21
|
+
#getBaseData() {
|
|
22
|
+
return this.#data.getData();
|
|
23
|
+
}
|
|
24
|
+
#getBaseId() {
|
|
25
|
+
if ('id' in this.#data && typeof this.#data.id === 'string') {
|
|
26
|
+
return this.#data.id;
|
|
27
|
+
}
|
|
28
|
+
return this.#getBaseData().id;
|
|
29
|
+
}
|
|
30
|
+
#getBaseType() {
|
|
31
|
+
if ('type' in this.#data && typeof this.#data.type === 'string') {
|
|
32
|
+
return this.#data.type;
|
|
33
|
+
}
|
|
34
|
+
return this.#getBaseData().type;
|
|
35
|
+
}
|
|
36
|
+
#getBaseTimeline() {
|
|
37
|
+
if ('timeline' in this.#data && this.#data.timeline) {
|
|
38
|
+
return this.#data.timeline;
|
|
39
|
+
}
|
|
40
|
+
return this.#getBaseData().timeline;
|
|
41
|
+
}
|
|
42
|
+
#getBaseSourceStartAt() {
|
|
43
|
+
if ('sourceStartAt' in this.#data) {
|
|
44
|
+
return this.#data.sourceStartAt;
|
|
45
|
+
}
|
|
46
|
+
const data = this.#getBaseData();
|
|
47
|
+
if (data.type === 'VIDEO' || data.type === 'AUDIO') {
|
|
48
|
+
return data.source.startAt ?? undefined;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
19
52
|
get duration() {
|
|
20
53
|
// TODO maybe we will need to get duration from the contextData
|
|
21
54
|
return this.#data.duration;
|
|
@@ -27,23 +60,23 @@ export class ComponentContext {
|
|
|
27
60
|
return this.#data.getData();
|
|
28
61
|
}
|
|
29
62
|
get id() {
|
|
30
|
-
return this
|
|
63
|
+
return this.#contextData?.id ?? this.#getBaseId();
|
|
31
64
|
}
|
|
32
65
|
get type() {
|
|
33
|
-
return this
|
|
66
|
+
return this.#contextData?.type ?? this.#getBaseType();
|
|
34
67
|
}
|
|
35
68
|
get isActive() {
|
|
36
69
|
const currentTime = this.sceneState.currentTime;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return currentTime >= startAt - tolerance && currentTime <= endAt + tolerance;
|
|
70
|
+
const timeline = this.#contextData?.timeline ?? this.#getBaseTimeline();
|
|
71
|
+
const startAt = timeline.startAt;
|
|
72
|
+
const endAt = timeline.endAt;
|
|
73
|
+
return isTimeWithinTimeline(currentTime, startAt, endAt, TIMELINE_BOUNDARY_EPSILON);
|
|
42
74
|
}
|
|
43
75
|
get progress() {
|
|
44
76
|
const currentTime = this.sceneState.currentTime;
|
|
45
|
-
const
|
|
46
|
-
const
|
|
77
|
+
const timeline = this.#contextData?.timeline ?? this.#getBaseTimeline();
|
|
78
|
+
const startAt = timeline.startAt;
|
|
79
|
+
const endAt = timeline.endAt;
|
|
47
80
|
const duration = endAt - startAt;
|
|
48
81
|
if (duration === 0)
|
|
49
82
|
return 0;
|
|
@@ -52,10 +85,15 @@ export class ComponentContext {
|
|
|
52
85
|
return progress;
|
|
53
86
|
}
|
|
54
87
|
get currentComponentTime() {
|
|
55
|
-
const
|
|
88
|
+
const timeline = this.#contextData?.timeline ?? this.#getBaseTimeline();
|
|
89
|
+
const componentType = this.#contextData?.type ?? this.#getBaseType();
|
|
90
|
+
const startAt = this.state.transformTime(timeline.startAt);
|
|
56
91
|
let startAtModifier = 0;
|
|
57
|
-
if (
|
|
58
|
-
|
|
92
|
+
if (componentType === 'VIDEO' || componentType === 'AUDIO') {
|
|
93
|
+
const overrideSourceStartAt = this.#contextData?.type === 'VIDEO' || this.#contextData?.type === 'AUDIO'
|
|
94
|
+
? this.#contextData.source.startAt
|
|
95
|
+
: undefined;
|
|
96
|
+
startAtModifier = overrideSourceStartAt ?? this.#getBaseSourceStartAt() ?? 0;
|
|
59
97
|
}
|
|
60
98
|
// Calculate relative time from start of component
|
|
61
99
|
const relativeTime = this.sceneState.currentTime - startAt;
|
|
@@ -63,13 +101,13 @@ export class ComponentContext {
|
|
|
63
101
|
return this.state.transformTime(startAtModifier, true) + relativeTime;
|
|
64
102
|
}
|
|
65
103
|
get componentTimelineTime() {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
if (relativeTime < 0 || relativeTime > duration) {
|
|
104
|
+
const timeline = this.#contextData?.timeline ?? this.#getBaseTimeline();
|
|
105
|
+
const startAt = timeline.startAt;
|
|
106
|
+
const endAt = timeline.endAt;
|
|
107
|
+
if (!isTimeWithinTimeline(this.sceneState.currentTime, startAt, endAt, TIMELINE_BOUNDARY_EPSILON)) {
|
|
71
108
|
return undefined;
|
|
72
109
|
}
|
|
110
|
+
const relativeTime = this.sceneState.currentTime - startAt;
|
|
73
111
|
return this.state.transformTime(relativeTime);
|
|
74
112
|
}
|
|
75
113
|
get currentTime() {
|
|
@@ -94,25 +132,32 @@ export class ComponentContext {
|
|
|
94
132
|
this.resources.delete(type);
|
|
95
133
|
}
|
|
96
134
|
async runHooks(hooks, type) {
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
const sortedHooks = [...hooks];
|
|
136
|
+
for (let i = 1; i < sortedHooks.length; i += 1) {
|
|
137
|
+
if ((sortedHooks[i - 1].priority ?? 0) > (sortedHooks[i].priority ?? 0)) {
|
|
138
|
+
sortedHooks.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
99
142
|
for (let i = 0; i < sortedHooks.length; i += 1) {
|
|
100
143
|
const handler = sortedHooks[i];
|
|
101
144
|
try {
|
|
102
145
|
await handler.handle(type, this);
|
|
103
146
|
}
|
|
104
147
|
catch (error) {
|
|
105
|
-
|
|
148
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
106
149
|
const hookName = handler.constructor?.name ?? `Hook[${i}]`;
|
|
107
|
-
console.warn(`[ComponentContext] Hook "${hookName}" failed during "${type}" for component "${this.id}":`,
|
|
108
|
-
// Emit error event for debugging/monitoring
|
|
150
|
+
console.warn(`[ComponentContext] Hook "${hookName}" failed during "${type}" for component "${this.id}":`, normalizedError.message);
|
|
109
151
|
this.eventManager.emit('hookerror', {
|
|
110
152
|
hookName,
|
|
111
153
|
hookType: type,
|
|
112
|
-
error:
|
|
154
|
+
error: normalizedError,
|
|
113
155
|
componentId: this.id,
|
|
114
156
|
timestamp: Date.now()
|
|
115
157
|
});
|
|
158
|
+
if (normalizedError instanceof DeterministicRenderError) {
|
|
159
|
+
throw normalizedError;
|
|
160
|
+
}
|
|
116
161
|
}
|
|
117
162
|
}
|
|
118
163
|
}
|
|
@@ -38,7 +38,7 @@ export class AnimationHook {
|
|
|
38
38
|
const target = this.#context.resources.get('animationTarget');
|
|
39
39
|
const animationData = this.#context.resources.get('animationData') ??
|
|
40
40
|
{};
|
|
41
|
-
const animations =
|
|
41
|
+
const animations = (this.#context.contextData.animations.list ?? []).filter((animation) => animation.enabled !== false);
|
|
42
42
|
const customAnimations = WordHighlighterAnimationBuilder.build(this.#context.contextData, target, animationData, this.splitTextCache);
|
|
43
43
|
const lineAnimations = LineHighlighterAnimationBuilder.build(this.#context.contextData, target, animationData, this.splitTextCache);
|
|
44
44
|
animations.push(...customAnimations);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IComponentContext, IComponentHook, HookType } from '../..';
|
|
2
|
+
import type { StateManager } from '../../managers/StateManager.svelte.js';
|
|
3
|
+
/** Renders COLOR and GRADIENT schema components into a Pixi texture source. */
|
|
4
|
+
export declare class CanvasFillHook implements IComponentHook {
|
|
5
|
+
#private;
|
|
6
|
+
types: HookType[];
|
|
7
|
+
priority: number;
|
|
8
|
+
constructor(cradle: {
|
|
9
|
+
stateManager: StateManager;
|
|
10
|
+
});
|
|
11
|
+
handle(type: HookType, context: IComponentContext): Promise<void>;
|
|
12
|
+
}
|