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.
Files changed (139) hide show
  1. package/README.md +122 -0
  2. package/bin/visualfries.js +1570 -0
  3. package/dist/DIContainer.d.ts +2 -1
  4. package/dist/DIContainer.js +16 -5
  5. package/dist/SceneBuilder.svelte.d.ts +70 -1
  6. package/dist/SceneBuilder.svelte.js +96 -29
  7. package/dist/agent/audioMixer.d.ts +34 -0
  8. package/dist/agent/audioMixer.js +308 -0
  9. package/dist/agent/broll.d.ts +35 -0
  10. package/dist/agent/broll.js +226 -0
  11. package/dist/agent/capabilities.d.ts +34 -0
  12. package/dist/agent/capabilities.js +246 -0
  13. package/dist/agent/captionScene.d.ts +23 -0
  14. package/dist/agent/captionScene.js +204 -0
  15. package/dist/agent/catalog.d.ts +24 -0
  16. package/dist/agent/catalog.js +67 -0
  17. package/dist/agent/cueFiles.d.ts +54 -0
  18. package/dist/agent/cueFiles.js +219 -0
  19. package/dist/agent/cuePresets.d.ts +7 -0
  20. package/dist/agent/cuePresets.js +50 -0
  21. package/dist/agent/deterministicMedia.d.ts +61 -0
  22. package/dist/agent/deterministicMedia.js +303 -0
  23. package/dist/agent/index.d.ts +18 -0
  24. package/dist/agent/index.js +18 -0
  25. package/dist/agent/inspectScene.d.ts +36 -0
  26. package/dist/agent/inspectScene.js +139 -0
  27. package/dist/agent/localRender.d.ts +82 -0
  28. package/dist/agent/localRender.js +702 -0
  29. package/dist/agent/overlays.d.ts +40 -0
  30. package/dist/agent/overlays.js +325 -0
  31. package/dist/agent/productionPlan.d.ts +131 -0
  32. package/dist/agent/productionPlan.js +322 -0
  33. package/dist/agent/renderPlan.d.ts +27 -0
  34. package/dist/agent/renderPlan.js +68 -0
  35. package/dist/agent/renderRanges.d.ts +24 -0
  36. package/dist/agent/renderRanges.js +73 -0
  37. package/dist/agent/runtimeSupport.d.ts +19 -0
  38. package/dist/agent/runtimeSupport.js +192 -0
  39. package/dist/agent/streamingEncoder.d.ts +31 -0
  40. package/dist/agent/streamingEncoder.js +208 -0
  41. package/dist/agent/transcripts.d.ts +29 -0
  42. package/dist/agent/transcripts.js +146 -0
  43. package/dist/agent/transitions.d.ts +20 -0
  44. package/dist/agent/transitions.js +169 -0
  45. package/dist/animations/PixiAnimationTarget.d.ts +29 -0
  46. package/dist/animations/PixiAnimationTarget.js +63 -0
  47. package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
  48. package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
  49. package/dist/browser/browserExport.d.ts +73 -0
  50. package/dist/browser/browserExport.js +180 -0
  51. package/dist/browser/glTransitions.d.ts +37 -0
  52. package/dist/browser/glTransitions.js +261 -0
  53. package/dist/browser/index.d.ts +3 -0
  54. package/dist/browser/index.js +3 -0
  55. package/dist/browser/mediaDecode.d.ts +31 -0
  56. package/dist/browser/mediaDecode.js +152 -0
  57. package/dist/builders/PixiComponentBuilder.d.ts +6 -0
  58. package/dist/builders/PixiComponentBuilder.js +12 -0
  59. package/dist/builders/_ComponentState.svelte.d.ts +7 -5
  60. package/dist/builders/_ComponentState.svelte.js +29 -15
  61. package/dist/builders/html/StyleBuilder.js +9 -8
  62. package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
  63. package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
  64. package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
  65. package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
  66. package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
  67. package/dist/commands/SeekCommand.js +19 -6
  68. package/dist/components/AnimatedGIF.d.ts +3 -0
  69. package/dist/components/AnimatedGIF.js +28 -1
  70. package/dist/components/Component.svelte.d.ts +5 -5
  71. package/dist/components/Component.svelte.js +29 -13
  72. package/dist/components/ComponentContext.svelte.js +68 -23
  73. package/dist/components/hooks/AnimationHook.js +1 -1
  74. package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
  75. package/dist/components/hooks/CanvasFillHook.js +75 -0
  76. package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
  77. package/dist/components/hooks/HtmlTextHook.js +11 -3
  78. package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
  79. package/dist/components/hooks/MediaHook.js +172 -47
  80. package/dist/components/hooks/MediaSeekingHook.js +138 -88
  81. package/dist/components/hooks/PixiGifHook.js +4 -4
  82. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.d.ts +3 -0
  83. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +85 -8
  84. package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
  85. package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
  86. package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
  87. package/dist/components/hooks/SubtitlesHook.js +48 -24
  88. package/dist/components/hooks/VerifyMediaHook.js +2 -2
  89. package/dist/directors/ComponentDirector.d.ts +1 -0
  90. package/dist/directors/ComponentDirector.js +41 -8
  91. package/dist/factories/SceneBuilderFactory.d.ts +3 -0
  92. package/dist/factories/SceneBuilderFactory.js +6 -0
  93. package/dist/fonts/fontDiscovery.js +12 -1
  94. package/dist/layers/Layer.svelte.d.ts +54 -0
  95. package/dist/layers/Layer.svelte.js +2 -25
  96. package/dist/managers/AppManager.svelte.d.ts +12 -0
  97. package/dist/managers/AppManager.svelte.js +180 -8
  98. package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
  99. package/dist/managers/ComponentsManager.svelte.js +29 -15
  100. package/dist/managers/DeterministicMediaManager.d.ts +6 -0
  101. package/dist/managers/DeterministicMediaManager.js +57 -3
  102. package/dist/managers/EventManager.js +3 -1
  103. package/dist/managers/LayersManager.svelte.d.ts +54 -0
  104. package/dist/managers/RenderManager.d.ts +5 -0
  105. package/dist/managers/RenderManager.js +67 -21
  106. package/dist/managers/StateManager.svelte.d.ts +56 -0
  107. package/dist/managers/StateManager.svelte.js +60 -58
  108. package/dist/managers/SubtitlesManager.svelte.js +1 -3
  109. package/dist/managers/TimeManager.svelte.d.ts +4 -0
  110. package/dist/managers/TimeManager.svelte.js +23 -7
  111. package/dist/schemas/runtime/deterministic.d.ts +3 -0
  112. package/dist/schemas/runtime/deterministic.js +5 -1
  113. package/dist/schemas/runtime/types.d.ts +36 -15
  114. package/dist/schemas/scene/components.d.ts +126 -0
  115. package/dist/schemas/scene/components.js +11 -0
  116. package/dist/schemas/scene/core.d.ts +109 -10
  117. package/dist/schemas/scene/core.js +1 -1
  118. package/dist/utils/mediaWindow.d.ts +13 -0
  119. package/dist/utils/mediaWindow.js +25 -0
  120. package/dist/utils/svgGenerator.js +1 -1
  121. package/dist/utils/timelineWindow.d.ts +2 -0
  122. package/dist/utils/timelineWindow.js +7 -0
  123. package/dist/utils/utils.js +2 -4
  124. package/docs/AGENT_PATTERNS.md +243 -0
  125. package/docs/AGENT_WORKFLOW.md +364 -0
  126. package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
  127. package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
  128. package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
  129. package/docs/DETERMINISTIC_RENDERER.md +83 -0
  130. package/docs/NEXT_RELEASE.md +28 -0
  131. package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
  132. package/docs/examples/agent-cues.json +49 -0
  133. package/docs/examples/agent-transcript.json +23 -0
  134. package/docs/examples/agent-transcript.srt +7 -0
  135. package/docs/examples/agent-transcript.vtt +7 -0
  136. package/docs/examples/image-animation.json +87 -0
  137. package/docs/examples/native-text-metric-badge.json +109 -0
  138. package/package.json +133 -100
  139. 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
- preset: TextEffectPresetName.DEFAULT,
50
+ structured: true,
51
51
  enabled: textAppearance.shadow.enabled ? true : false,
52
- color: typeof textAppearance.color === 'string'
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
- preset: TextEffectPresetName.OUTLINE,
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')}em`;
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
- const shadow = TextShadowBuilder.build({
28
- preset: TextEffectPresetName.OUTLINE,
29
- size,
30
- color,
31
- opacity: data.opacity || 1
32
- });
33
- if (shadow && shadow !== 'none') {
34
- shadowStrings.push(shadow);
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 || 1
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.length}`;
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') || component.context.getResource('imageElement');
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.round(currentTime * fps));
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
- throw new Error(`Deterministic media was not ready after seek for active components: ${pending.join(', ')}`);
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._currentTime / this.duration;
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>): Promise<Component>;
24
+ updateAppearance(appearance: Partial<AppearanceInput>): Component;
25
25
  setStart(start: number): Component;
26
26
  setEnd(end: number): Component;
27
- updateText(text: string): Promise<Component>;
28
- setText(text: string): Promise<Component>;
29
- setVisible(visible: boolean): Promise<Component>;
30
- setOrder(order: number): Promise<Component>;
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 ? priority : maxPriority + 1;
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
- const hooks = this.#hooks.filter((hook) => hook.types.includes(type));
55
- await this.#context.runHooks(hooks, type);
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
- async updateAppearance(appearance) {
82
- await this.#properties.updateAppearance(appearance);
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
- async updateText(text) {
94
- await this.#properties.updateText(text);
109
+ updateText(text) {
110
+ this.#properties.updateText(text);
95
111
  return this;
96
112
  }
97
113
  // Simple fluent wrapper methods
98
- async setText(text) {
114
+ setText(text) {
99
115
  return this.updateText(text);
100
116
  }
101
- async setVisible(visible) {
102
- await this.#properties.setVisible(visible);
117
+ setVisible(visible) {
118
+ this.#properties.setVisible(visible);
103
119
  return this;
104
120
  }
105
- async setOrder(order) {
106
- await this.#properties.setOrder(order);
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.data.id;
63
+ return this.#contextData?.id ?? this.#getBaseId();
31
64
  }
32
65
  get type() {
33
- return this.data.type;
66
+ return this.#contextData?.type ?? this.#getBaseType();
34
67
  }
35
68
  get isActive() {
36
69
  const currentTime = this.sceneState.currentTime;
37
- const startAt = this.data.timeline.startAt;
38
- const endAt = this.data.timeline.endAt;
39
- // Add small tolerance to handle floating point precision issues
40
- const tolerance = 1e-10;
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 startAt = this.data.timeline.startAt;
46
- const endAt = this.data.timeline.endAt;
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 startAt = this.state.transformTime(this.data.timeline.startAt);
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 (this.data.type === 'VIDEO') {
58
- startAtModifier = this.data.source.startAt ?? 0;
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 startAt = this.data.timeline.startAt;
67
- const endAt = this.data.timeline.endAt;
68
- const duration = endAt - startAt;
69
- const relativeTime = this.sceneState.currentTime - startAt;
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
- // Sort handlers by priority (low to high, meaning 1 has higher priority then 100)
98
- const sortedHooks = [...hooks].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
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
- // Log the error but continue to next hook
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}":`, error instanceof Error ? error.message : String(error));
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: error instanceof Error ? error : new Error(String(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 = [...(this.#context.contextData.animations.list ?? [])];
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
+ }