visualfries 0.1.902 → 0.1.1096
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.js +1 -6
- package/dist/animations/builders/WordHighlighterAnimationBuilder.js +5 -0
- package/dist/builders/_ComponentState.svelte.js +32 -19
- package/dist/commands/SeekCommand.js +20 -0
- package/dist/components/ComponentContext.svelte.js +16 -1
- package/dist/components/ComponentContextHelpers.d.ts +115 -0
- package/dist/components/ComponentContextHelpers.js +196 -0
- package/dist/components/SafeHookRunner.d.ts +52 -0
- package/dist/components/SafeHookRunner.js +67 -0
- package/dist/components/hooks/HtmlToCanvasHook.js +24 -1
- package/dist/components/hooks/MediaHook.js +1 -1
- package/dist/components/hooks/PixiDisplayObjectHook.js +19 -3
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +28 -10
- package/dist/components/hooks/PixiVideoTextureHook.js +41 -13
- package/dist/directors/ComponentDirector.js +1 -1
- package/dist/examples/01_basic_text.json +89 -0
- package/dist/examples/02_animated_text.json +110 -0
- package/dist/examples/03_video_background.json +94 -0
- package/dist/examples/04_real_subtitles.json +116 -0
- package/dist/examples/animated-shapes.json +148 -0
- package/dist/examples/gradient-background.json +105 -0
- package/dist/examples/karaoke-subtitles.json +97 -0
- package/dist/managers/SubtitlesManager.svelte.js +3 -1
- package/dist/schemas/runtime/types.d.ts +7 -0
- package/dist/utils/utils.js +22 -4
- package/package.json +1 -1
|
@@ -264,16 +264,11 @@ export class SceneBuilder {
|
|
|
264
264
|
if (!layer) {
|
|
265
265
|
return false;
|
|
266
266
|
}
|
|
267
|
-
// Create clone of component data
|
|
268
|
-
const originalEndAt = component.props.timeline.endAt;
|
|
267
|
+
// Create clone of component data EXACTLY as it is
|
|
269
268
|
const newData = changeIdDeep(component.props.getData());
|
|
270
269
|
const cloneData = {
|
|
271
270
|
...newData,
|
|
272
271
|
id: uuidv4(), // Generate new ID for clone
|
|
273
|
-
timeline: {
|
|
274
|
-
...newData.timeline,
|
|
275
|
-
endAt: originalEndAt
|
|
276
|
-
},
|
|
277
272
|
checksum: 'new-' + uuidv4()
|
|
278
273
|
};
|
|
279
274
|
// Update original component's end time
|
|
@@ -110,6 +110,11 @@ export class WordHighlighterAnimationBuilder {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
static createBackgroundElement(target) {
|
|
113
|
+
// Clean up any existing highlighter-bg element from previous builds
|
|
114
|
+
const existingBg = target.querySelector('#highlighter-bg');
|
|
115
|
+
if (existingBg) {
|
|
116
|
+
existingBg.remove();
|
|
117
|
+
}
|
|
113
118
|
const bgHighlighter = document.createElement('div');
|
|
114
119
|
bgHighlighter.id = 'highlighter-bg';
|
|
115
120
|
bgHighlighter.style.position = 'absolute';
|
|
@@ -108,39 +108,42 @@ export class ComponentState {
|
|
|
108
108
|
this.#emitChange();
|
|
109
109
|
}
|
|
110
110
|
#changeVideoStart(diff) {
|
|
111
|
-
if (this.type === 'VIDEO') {
|
|
111
|
+
if (this.type === 'VIDEO' || this.type === 'AUDIO') {
|
|
112
112
|
let source = this.#data.source;
|
|
113
113
|
if (!source) {
|
|
114
|
-
source = {
|
|
115
|
-
startAt: 0
|
|
116
|
-
};
|
|
114
|
+
source = { startAt: 0 };
|
|
117
115
|
this.#data.source = source;
|
|
118
116
|
}
|
|
119
117
|
if (source.startAt !== undefined && source.startAt !== null) {
|
|
120
118
|
source.startAt += diff;
|
|
121
119
|
source.startAt = Math.max(0, source.startAt);
|
|
122
120
|
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
#changeVideoEnd(diff) {
|
|
124
|
+
if (this.type === 'VIDEO' || this.type === 'AUDIO') {
|
|
125
|
+
let source = this.#data.source;
|
|
126
|
+
if (!source) {
|
|
127
|
+
source = { startAt: 0 };
|
|
128
|
+
this.#data.source = source;
|
|
129
|
+
}
|
|
130
|
+
const currentDuration = this.#data.timeline.endAt - this.#data.timeline.startAt;
|
|
123
131
|
if (source.endAt !== undefined && source.endAt !== null) {
|
|
124
132
|
source.endAt += diff;
|
|
125
133
|
}
|
|
134
|
+
else {
|
|
135
|
+
// Initialize the implicit end bound via start bound prior to truncation
|
|
136
|
+
const startAt = source.startAt || 0;
|
|
137
|
+
source.endAt = startAt + currentDuration + diff;
|
|
138
|
+
}
|
|
126
139
|
}
|
|
127
|
-
// TODO verify starting time is not beyond video duration
|
|
128
|
-
// const metadata = this.#data!.metadata
|
|
129
|
-
// ? (this.#data!.metadata as Metadata)
|
|
130
|
-
// : ({ starting_time: 0 } as Metadata);
|
|
131
|
-
// let startingTime = metadata.starting_time ? (metadata.starting_time as number) : 0;
|
|
132
|
-
// startingTime += diff;
|
|
133
|
-
// startingTime = Math.max(0, startingTime);
|
|
134
|
-
// this.updateMetadata({
|
|
135
|
-
// starting_time: startingTime
|
|
136
|
-
// });
|
|
137
140
|
}
|
|
138
141
|
setStart(start) {
|
|
139
142
|
const beforeStart = this.sceneState.transformTime(this.#data.timeline.startAt);
|
|
140
143
|
const newStart = this.sceneState.transformTime(start);
|
|
141
144
|
const diff = newStart - beforeStart;
|
|
142
145
|
if (diff !== 0) {
|
|
143
|
-
if (this.type === 'VIDEO') {
|
|
146
|
+
if (this.type === 'VIDEO' || this.type === 'AUDIO') {
|
|
144
147
|
this.#changeVideoStart(diff);
|
|
145
148
|
}
|
|
146
149
|
this.#data.timeline.startAt = this.sceneState.transformTime(start);
|
|
@@ -148,8 +151,16 @@ export class ComponentState {
|
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
setEnd(end) {
|
|
151
|
-
|
|
152
|
-
this
|
|
154
|
+
const beforeEnd = this.sceneState.transformTime(this.#data.timeline.endAt);
|
|
155
|
+
const newEnd = this.sceneState.transformTime(end);
|
|
156
|
+
const diff = newEnd - beforeEnd;
|
|
157
|
+
if (diff !== 0) {
|
|
158
|
+
if (this.type === 'VIDEO' || this.type === 'AUDIO') {
|
|
159
|
+
this.#changeVideoEnd(diff);
|
|
160
|
+
}
|
|
161
|
+
this.#data.timeline.endAt = this.sceneState.transformTime(end);
|
|
162
|
+
this.#emitChange();
|
|
163
|
+
}
|
|
153
164
|
}
|
|
154
165
|
setStreamPath(path) {
|
|
155
166
|
// if (this.type === 'VIDEO' || this.type === 'AUDIO') {
|
|
@@ -179,8 +190,10 @@ export class ComponentState {
|
|
|
179
190
|
// }
|
|
180
191
|
}
|
|
181
192
|
async updateAppearance(appearance) {
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
// Use $state.snapshot() to properly extract all properties from the reactive proxy
|
|
194
|
+
const currentData = $state.snapshot(this.#data);
|
|
195
|
+
const mergedAppearance = merge({}, currentData.appearance, appearance);
|
|
196
|
+
this.#data = { ...currentData, appearance: mergedAppearance };
|
|
184
197
|
this.#emitChange();
|
|
185
198
|
await this.maybeAutoRefresh();
|
|
186
199
|
}
|
|
@@ -23,6 +23,21 @@ export class SeekCommand {
|
|
|
23
23
|
this.timeline.seek(time);
|
|
24
24
|
// Ensure a deterministic render on server after seek to advance media frames
|
|
25
25
|
if (this.state.environment === 'server') {
|
|
26
|
+
// Wait for fonts to be ready before rendering
|
|
27
|
+
// This is critical for subtitle animations that use SplitText -
|
|
28
|
+
// if fonts aren't loaded, text measurements will be wrong and
|
|
29
|
+
// animations may fail silently or not appear on first subtitles
|
|
30
|
+
if (typeof document !== 'undefined' && document.fonts?.ready) {
|
|
31
|
+
try {
|
|
32
|
+
await Promise.race([
|
|
33
|
+
document.fonts.ready,
|
|
34
|
+
new Promise((resolve) => setTimeout(resolve, 2000)) // 2s timeout
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Ignore font loading errors, continue with rendering
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
// Try multiple render passes until loading state clears or attempts exhausted
|
|
27
42
|
const maxAttempts = 10;
|
|
28
43
|
for (let i = 0; i < maxAttempts; i += 1) {
|
|
@@ -34,6 +49,11 @@ export class SeekCommand {
|
|
|
34
49
|
if (this.state.state === 'loading') {
|
|
35
50
|
console.warn('SeekCommand: Max render attempts exhausted while still loading');
|
|
36
51
|
}
|
|
52
|
+
// Re-seek to apply correct animation state to any animations
|
|
53
|
+
// that were added during the render passes above.
|
|
54
|
+
// This fixes the race condition where subtitle animations are added
|
|
55
|
+
// AFTER the initial seek, causing them to miss their initial state.
|
|
56
|
+
this.timeline.seek(time);
|
|
37
57
|
}
|
|
38
58
|
}
|
|
39
59
|
}
|
|
@@ -98,7 +98,22 @@ export class ComponentContext {
|
|
|
98
98
|
const sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
99
99
|
for (let i = 0; i < sortedHooks.length; i += 1) {
|
|
100
100
|
const handler = sortedHooks[i];
|
|
101
|
-
|
|
101
|
+
try {
|
|
102
|
+
await handler.handle(type, this);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// Log the error but continue to next hook
|
|
106
|
+
const hookName = handler.constructor?.name ?? `Hook[${i}]`;
|
|
107
|
+
console.warn(`[ComponentContext] Hook "${hookName}" failed during "${type}" for component "${this.id}":`, error instanceof Error ? error.message : String(error));
|
|
108
|
+
// Emit error event for debugging/monitoring
|
|
109
|
+
this.eventManager.emit('hookerror', {
|
|
110
|
+
hookName,
|
|
111
|
+
hookType: type,
|
|
112
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
113
|
+
componentId: this.id,
|
|
114
|
+
timestamp: Date.now()
|
|
115
|
+
});
|
|
116
|
+
}
|
|
102
117
|
}
|
|
103
118
|
}
|
|
104
119
|
destroy() { }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Context Helpers
|
|
3
|
+
*
|
|
4
|
+
* Type guards and helper functions for type-safe access to component-specific data
|
|
5
|
+
* in hook contexts. Eliminates the need for `as any` casts throughout the codebase.
|
|
6
|
+
*/
|
|
7
|
+
import type { Component, VideoComponent, AudioComponent, ImageComponent, GifComponent, TextComponent, SubtitleComponent, ShapeComponent, ColorComponent, GradientComponent, ComponentSource, IComponentContext } from '..';
|
|
8
|
+
export declare const MEDIA_COMPONENT_TYPES: readonly ["VIDEO", "AUDIO"];
|
|
9
|
+
export declare const SOURCE_COMPONENT_TYPES: readonly ["VIDEO", "AUDIO", "IMAGE", "GIF"];
|
|
10
|
+
export declare const VISUAL_COMPONENT_TYPES: readonly ["VIDEO", "IMAGE", "GIF", "TEXT", "SHAPE", "SUBTITLES", "COLOR", "GRADIENT"];
|
|
11
|
+
export type MediaComponentType = (typeof MEDIA_COMPONENT_TYPES)[number];
|
|
12
|
+
export type SourceComponentType = (typeof SOURCE_COMPONENT_TYPES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Type guard for VIDEO component
|
|
15
|
+
*/
|
|
16
|
+
export declare function isVideoComponent(data: Component | undefined): data is VideoComponent;
|
|
17
|
+
/**
|
|
18
|
+
* Type guard for AUDIO component
|
|
19
|
+
*/
|
|
20
|
+
export declare function isAudioComponent(data: Component | undefined): data is AudioComponent;
|
|
21
|
+
/**
|
|
22
|
+
* Type guard for IMAGE component
|
|
23
|
+
*/
|
|
24
|
+
export declare function isImageComponent(data: Component | undefined): data is ImageComponent;
|
|
25
|
+
/**
|
|
26
|
+
* Type guard for GIF component
|
|
27
|
+
*/
|
|
28
|
+
export declare function isGifComponent(data: Component | undefined): data is GifComponent;
|
|
29
|
+
/**
|
|
30
|
+
* Type guard for TEXT component
|
|
31
|
+
*/
|
|
32
|
+
export declare function isTextComponent(data: Component | undefined): data is TextComponent;
|
|
33
|
+
/**
|
|
34
|
+
* Type guard for SUBTITLES component
|
|
35
|
+
*/
|
|
36
|
+
export declare function isSubtitleComponent(data: Component | undefined): data is SubtitleComponent;
|
|
37
|
+
/**
|
|
38
|
+
* Type guard for SHAPE component
|
|
39
|
+
*/
|
|
40
|
+
export declare function isShapeComponent(data: Component | undefined): data is ShapeComponent;
|
|
41
|
+
/**
|
|
42
|
+
* Type guard for COLOR component
|
|
43
|
+
*/
|
|
44
|
+
export declare function isColorComponent(data: Component | undefined): data is ColorComponent;
|
|
45
|
+
/**
|
|
46
|
+
* Type guard for GRADIENT component
|
|
47
|
+
*/
|
|
48
|
+
export declare function isGradientComponent(data: Component | undefined): data is GradientComponent;
|
|
49
|
+
/**
|
|
50
|
+
* Type guard for components with a media source (VIDEO, AUDIO, IMAGE, GIF)
|
|
51
|
+
*/
|
|
52
|
+
export declare function hasSource(data: Component | undefined): data is VideoComponent | AudioComponent | ImageComponent | GifComponent;
|
|
53
|
+
/**
|
|
54
|
+
* Type guard for media components (VIDEO, AUDIO)
|
|
55
|
+
*/
|
|
56
|
+
export declare function isMediaComponent(data: Component | undefined): data is VideoComponent | AudioComponent;
|
|
57
|
+
/**
|
|
58
|
+
* Safely get source from component data
|
|
59
|
+
*/
|
|
60
|
+
export declare function getSource(data: Component | undefined): ComponentSource | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Safely get source.url from component data
|
|
63
|
+
*/
|
|
64
|
+
export declare function getSourceUrl(data: Component | undefined): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Safely get source.startAt from component data (for VIDEO/AUDIO)
|
|
67
|
+
*/
|
|
68
|
+
export declare function getSourceStartAt(data: Component | undefined): number | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Safely get muted state from media component
|
|
71
|
+
*/
|
|
72
|
+
export declare function getMuted(data: Component | undefined): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Safely get volume from media component
|
|
75
|
+
*/
|
|
76
|
+
export declare function getVolume(data: Component | undefined): number;
|
|
77
|
+
/**
|
|
78
|
+
* Safely get text content from TEXT component
|
|
79
|
+
*/
|
|
80
|
+
export declare function getTextContent(data: Component | undefined): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Get typed component data from context
|
|
83
|
+
*/
|
|
84
|
+
export declare function getContextData<T extends Component>(context: IComponentContext, typeGuard: (data: Component | undefined) => data is T): T | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Get video component data from context
|
|
87
|
+
*/
|
|
88
|
+
export declare function getVideoData(context: IComponentContext): VideoComponent | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Get audio component data from context
|
|
91
|
+
*/
|
|
92
|
+
export declare function getAudioData(context: IComponentContext): AudioComponent | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Get image component data from context
|
|
95
|
+
*/
|
|
96
|
+
export declare function getImageData(context: IComponentContext): ImageComponent | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Get gif component data from context
|
|
99
|
+
*/
|
|
100
|
+
export declare function getGifData(context: IComponentContext): GifComponent | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Get text component data from context
|
|
103
|
+
*/
|
|
104
|
+
export declare function getTextData(context: IComponentContext): TextComponent | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Get source from context's component data
|
|
107
|
+
*/
|
|
108
|
+
export declare function getContextSource(context: IComponentContext): ComponentSource | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Get media properties (muted, volume) from context
|
|
111
|
+
*/
|
|
112
|
+
export declare function getMediaProps(context: IComponentContext): {
|
|
113
|
+
muted: boolean;
|
|
114
|
+
volume: number;
|
|
115
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Context Helpers
|
|
3
|
+
*
|
|
4
|
+
* Type guards and helper functions for type-safe access to component-specific data
|
|
5
|
+
* in hook contexts. Eliminates the need for `as any` casts throughout the codebase.
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Component Type Constants
|
|
9
|
+
// ============================================================================
|
|
10
|
+
export const MEDIA_COMPONENT_TYPES = ['VIDEO', 'AUDIO'];
|
|
11
|
+
export const SOURCE_COMPONENT_TYPES = ['VIDEO', 'AUDIO', 'IMAGE', 'GIF'];
|
|
12
|
+
export const VISUAL_COMPONENT_TYPES = ['VIDEO', 'IMAGE', 'GIF', 'TEXT', 'SHAPE', 'SUBTITLES', 'COLOR', 'GRADIENT'];
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Type Guards for Component Data
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Type guard for VIDEO component
|
|
18
|
+
*/
|
|
19
|
+
export function isVideoComponent(data) {
|
|
20
|
+
return data?.type === 'VIDEO';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Type guard for AUDIO component
|
|
24
|
+
*/
|
|
25
|
+
export function isAudioComponent(data) {
|
|
26
|
+
return data?.type === 'AUDIO';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Type guard for IMAGE component
|
|
30
|
+
*/
|
|
31
|
+
export function isImageComponent(data) {
|
|
32
|
+
return data?.type === 'IMAGE';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Type guard for GIF component
|
|
36
|
+
*/
|
|
37
|
+
export function isGifComponent(data) {
|
|
38
|
+
return data?.type === 'GIF';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Type guard for TEXT component
|
|
42
|
+
*/
|
|
43
|
+
export function isTextComponent(data) {
|
|
44
|
+
return data?.type === 'TEXT';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Type guard for SUBTITLES component
|
|
48
|
+
*/
|
|
49
|
+
export function isSubtitleComponent(data) {
|
|
50
|
+
return data?.type === 'SUBTITLES';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Type guard for SHAPE component
|
|
54
|
+
*/
|
|
55
|
+
export function isShapeComponent(data) {
|
|
56
|
+
return data?.type === 'SHAPE';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Type guard for COLOR component
|
|
60
|
+
*/
|
|
61
|
+
export function isColorComponent(data) {
|
|
62
|
+
return data?.type === 'COLOR';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Type guard for GRADIENT component
|
|
66
|
+
*/
|
|
67
|
+
export function isGradientComponent(data) {
|
|
68
|
+
return data?.type === 'GRADIENT';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Type guard for components with a media source (VIDEO, AUDIO, IMAGE, GIF)
|
|
72
|
+
*/
|
|
73
|
+
export function hasSource(data) {
|
|
74
|
+
return (data !== undefined &&
|
|
75
|
+
SOURCE_COMPONENT_TYPES.includes(data.type));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Type guard for media components (VIDEO, AUDIO)
|
|
79
|
+
*/
|
|
80
|
+
export function isMediaComponent(data) {
|
|
81
|
+
return (data !== undefined &&
|
|
82
|
+
MEDIA_COMPONENT_TYPES.includes(data.type));
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Safe Accessor Helpers
|
|
86
|
+
// ============================================================================
|
|
87
|
+
/**
|
|
88
|
+
* Safely get source from component data
|
|
89
|
+
*/
|
|
90
|
+
export function getSource(data) {
|
|
91
|
+
if (hasSource(data)) {
|
|
92
|
+
return data.source;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Safely get source.url from component data
|
|
98
|
+
*/
|
|
99
|
+
export function getSourceUrl(data) {
|
|
100
|
+
return getSource(data)?.url;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Safely get source.startAt from component data (for VIDEO/AUDIO)
|
|
104
|
+
*/
|
|
105
|
+
export function getSourceStartAt(data) {
|
|
106
|
+
const startAt = getSource(data)?.startAt;
|
|
107
|
+
// Convert null to undefined for consistency
|
|
108
|
+
return startAt ?? undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Safely get muted state from media component
|
|
112
|
+
*/
|
|
113
|
+
export function getMuted(data) {
|
|
114
|
+
if (isVideoComponent(data) || isAudioComponent(data)) {
|
|
115
|
+
return data.muted ?? false;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Safely get volume from media component
|
|
121
|
+
*/
|
|
122
|
+
export function getVolume(data) {
|
|
123
|
+
if (isVideoComponent(data) || isAudioComponent(data)) {
|
|
124
|
+
return data.volume ?? 1;
|
|
125
|
+
}
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Safely get text content from TEXT component
|
|
130
|
+
*/
|
|
131
|
+
export function getTextContent(data) {
|
|
132
|
+
if (isTextComponent(data)) {
|
|
133
|
+
return data.text;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Context Helper Functions
|
|
139
|
+
// ============================================================================
|
|
140
|
+
/**
|
|
141
|
+
* Get typed component data from context
|
|
142
|
+
*/
|
|
143
|
+
export function getContextData(context, typeGuard) {
|
|
144
|
+
const data = context.contextData;
|
|
145
|
+
if (typeGuard(data)) {
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get video component data from context
|
|
152
|
+
*/
|
|
153
|
+
export function getVideoData(context) {
|
|
154
|
+
return getContextData(context, isVideoComponent);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get audio component data from context
|
|
158
|
+
*/
|
|
159
|
+
export function getAudioData(context) {
|
|
160
|
+
return getContextData(context, isAudioComponent);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get image component data from context
|
|
164
|
+
*/
|
|
165
|
+
export function getImageData(context) {
|
|
166
|
+
return getContextData(context, isImageComponent);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get gif component data from context
|
|
170
|
+
*/
|
|
171
|
+
export function getGifData(context) {
|
|
172
|
+
return getContextData(context, isGifComponent);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get text component data from context
|
|
176
|
+
*/
|
|
177
|
+
export function getTextData(context) {
|
|
178
|
+
return getContextData(context, isTextComponent);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get source from context's component data
|
|
182
|
+
*/
|
|
183
|
+
export function getContextSource(context) {
|
|
184
|
+
const data = context.contextData;
|
|
185
|
+
return getSource(data);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get media properties (muted, volume) from context
|
|
189
|
+
*/
|
|
190
|
+
export function getMediaProps(context) {
|
|
191
|
+
const data = context.contextData;
|
|
192
|
+
return {
|
|
193
|
+
muted: getMuted(data),
|
|
194
|
+
volume: getVolume(data)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Hook Runner
|
|
3
|
+
*
|
|
4
|
+
* Utility for safely executing hooks with error boundaries.
|
|
5
|
+
* Prevents one failing hook from crashing the entire component lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
import type { EventManager } from '../managers/EventManager.js';
|
|
8
|
+
export interface HookError {
|
|
9
|
+
hookName: string;
|
|
10
|
+
hookType: string;
|
|
11
|
+
error: Error;
|
|
12
|
+
componentId: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
export interface SafeHookRunnerOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Whether to continue executing remaining hooks after one fails
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
continueOnError?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to log errors to console
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
logErrors?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Event manager to emit error events
|
|
28
|
+
*/
|
|
29
|
+
eventManager?: EventManager;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Safely execute a single hook handler with error boundary
|
|
33
|
+
*/
|
|
34
|
+
export declare function safeExecuteHook<T>(hookName: string, hookType: string, componentId: string, handler: () => Promise<T> | T, options?: SafeHookRunnerOptions): Promise<{
|
|
35
|
+
success: boolean;
|
|
36
|
+
result?: T;
|
|
37
|
+
error?: HookError;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Execute multiple hooks in sequence with error boundaries
|
|
41
|
+
*/
|
|
42
|
+
export declare function safeExecuteHooks(hooks: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
handler: () => Promise<void> | void;
|
|
45
|
+
}>, hookType: string, componentId: string, options?: SafeHookRunnerOptions): Promise<{
|
|
46
|
+
allSucceeded: boolean;
|
|
47
|
+
errors: HookError[];
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Create a wrapped version of a hook handler that catches errors
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSafeHandler<T extends (...args: any[]) => Promise<any> | any>(hookName: string, hookType: string, componentId: string, handler: T, options?: SafeHookRunnerOptions): T;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe Hook Runner
|
|
3
|
+
*
|
|
4
|
+
* Utility for safely executing hooks with error boundaries.
|
|
5
|
+
* Prevents one failing hook from crashing the entire component lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
const defaultOptions = {
|
|
8
|
+
continueOnError: true,
|
|
9
|
+
logErrors: true
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Safely execute a single hook handler with error boundary
|
|
13
|
+
*/
|
|
14
|
+
export async function safeExecuteHook(hookName, hookType, componentId, handler, options = {}) {
|
|
15
|
+
const opts = { ...defaultOptions, ...options };
|
|
16
|
+
try {
|
|
17
|
+
const result = await handler();
|
|
18
|
+
return { success: true, result };
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
22
|
+
const hookError = {
|
|
23
|
+
hookName,
|
|
24
|
+
hookType,
|
|
25
|
+
error,
|
|
26
|
+
componentId,
|
|
27
|
+
timestamp: Date.now()
|
|
28
|
+
};
|
|
29
|
+
if (opts.logErrors) {
|
|
30
|
+
console.warn(`[SafeHookRunner] Hook "${hookName}" failed during "${hookType}" for component "${componentId}":`, error.message);
|
|
31
|
+
}
|
|
32
|
+
if (opts.eventManager) {
|
|
33
|
+
opts.eventManager.emit('hookerror', hookError);
|
|
34
|
+
}
|
|
35
|
+
return { success: false, error: hookError };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute multiple hooks in sequence with error boundaries
|
|
40
|
+
*/
|
|
41
|
+
export async function safeExecuteHooks(hooks, hookType, componentId, options = {}) {
|
|
42
|
+
const opts = { ...defaultOptions, ...options };
|
|
43
|
+
const errors = [];
|
|
44
|
+
for (const hook of hooks) {
|
|
45
|
+
const result = await safeExecuteHook(hook.name, hookType, componentId, hook.handler, options);
|
|
46
|
+
if (!result.success && result.error) {
|
|
47
|
+
errors.push(result.error);
|
|
48
|
+
if (!opts.continueOnError) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
allSucceeded: errors.length === 0,
|
|
55
|
+
errors
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a wrapped version of a hook handler that catches errors
|
|
60
|
+
*/
|
|
61
|
+
export function createSafeHandler(hookName, hookType, componentId, handler, options = {}) {
|
|
62
|
+
const wrapped = async (...args) => {
|
|
63
|
+
const result = await safeExecuteHook(hookName, hookType, componentId, () => handler(...args), options);
|
|
64
|
+
return result.result;
|
|
65
|
+
};
|
|
66
|
+
return wrapped;
|
|
67
|
+
}
|
|
@@ -67,7 +67,30 @@ export class HtmlToCanvasHook {
|
|
|
67
67
|
}
|
|
68
68
|
const { width, height } = this.state;
|
|
69
69
|
if (!this.svgBase) {
|
|
70
|
-
|
|
70
|
+
// Safely encode characters list, filtering out any problematic characters
|
|
71
|
+
let encodedChars = '';
|
|
72
|
+
try {
|
|
73
|
+
const charsList = this.state.getCharactersList().join('');
|
|
74
|
+
encodedChars = encodeURIComponent(charsList);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// If encoding fails (e.g., unpaired surrogates), filter to only safe characters
|
|
78
|
+
console.warn('Failed to encode characters list, filtering to safe characters:', error);
|
|
79
|
+
const safeChars = this.state
|
|
80
|
+
.getCharactersList()
|
|
81
|
+
.filter((char) => {
|
|
82
|
+
try {
|
|
83
|
+
encodeURIComponent(char);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.join('');
|
|
91
|
+
encodedChars = encodeURIComponent(safeChars);
|
|
92
|
+
}
|
|
93
|
+
const { base, content, end } = await svgGenerator.generateSVG(this.#htmlEl, this.#context.data.appearance.text, width, height, 'svg-' + this.#context.contextData.id, encodedChars);
|
|
71
94
|
this.svgBase = base;
|
|
72
95
|
this.svgEnd = end;
|
|
73
96
|
this.svg = base + content + end;
|
|
@@ -2,7 +2,7 @@ import { MediaManager } from '../../managers/MediaManager.js';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { StateManager } from '../../managers/StateManager.svelte.js';
|
|
4
4
|
export class MediaHook {
|
|
5
|
-
types = ['setup', 'update', 'destroy'
|
|
5
|
+
types = ['setup', 'update', 'destroy'];
|
|
6
6
|
priority = 1;
|
|
7
7
|
#context;
|
|
8
8
|
#mediaElement;
|