@shotstack/shotstack-canvas 2.1.5 → 2.1.7

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.
@@ -271,11 +271,6 @@ declare const richCaptionAssetSchema: z.ZodObject<{
271
271
  color: z.ZodDefault<z.ZodString>;
272
272
  opacity: z.ZodDefault<z.ZodNumber>;
273
273
  background: z.ZodOptional<z.ZodString>;
274
- textDecoration: z.ZodDefault<z.ZodEnum<{
275
- none: "none";
276
- underline: "underline";
277
- "line-through": "line-through";
278
- }>>;
279
274
  }, z.core.$strip>>;
280
275
  style: z.ZodOptional<z.ZodObject<{
281
276
  wordSpacing: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>, z.ZodString]>>>;
@@ -356,39 +351,38 @@ declare const richCaptionAssetSchema: z.ZodObject<{
356
351
  "line-through": "line-through";
357
352
  }>>;
358
353
  }, z.core.$strip>>;
359
- stroke: z.ZodOptional<z.ZodObject<{
354
+ stroke: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
360
355
  width: z.ZodOptional<z.ZodNumber>;
361
356
  color: z.ZodOptional<z.ZodString>;
362
357
  opacity: z.ZodOptional<z.ZodNumber>;
363
- }, z.core.$strip>>;
364
- shadow: z.ZodOptional<z.ZodObject<{
358
+ }, z.core.$strip>, z.ZodLiteral<"none">]>>;
359
+ shadow: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
365
360
  offsetX: z.ZodOptional<z.ZodNumber>;
366
361
  offsetY: z.ZodOptional<z.ZodNumber>;
367
362
  blur: z.ZodOptional<z.ZodNumber>;
368
363
  color: z.ZodOptional<z.ZodString>;
369
364
  opacity: z.ZodOptional<z.ZodNumber>;
370
- }, z.core.$strip>>;
365
+ }, z.core.$strip>, z.ZodLiteral<"none">]>>;
371
366
  scale: z.ZodDefault<z.ZodNumber>;
372
367
  }, z.core.$strict>>;
373
368
  wordAnimation: z.ZodOptional<z.ZodObject<{
374
369
  style: z.ZodDefault<z.ZodEnum<{
375
370
  typewriter: "typewriter";
376
371
  none: "none";
372
+ pop: "pop";
377
373
  karaoke: "karaoke";
378
374
  highlight: "highlight";
379
- pop: "pop";
380
375
  fade: "fade";
381
376
  slide: "slide";
382
377
  bounce: "bounce";
383
378
  }>>;
384
- speed: z.ZodDefault<z.ZodNumber>;
385
379
  direction: z.ZodDefault<z.ZodEnum<{
386
380
  right: "right";
387
381
  left: "left";
388
382
  up: "up";
389
383
  down: "down";
390
384
  }>>;
391
- }, z.core.$strict>>;
385
+ }, z.core.$strip>>;
392
386
  pauseThreshold: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
393
387
  customFonts: z.ZodOptional<z.ZodArray<z.ZodObject<{
394
388
  src: z.ZodString;
@@ -414,11 +408,6 @@ declare const CanvasRichCaptionAssetSchema: z.ZodObject<{
414
408
  color: z.ZodDefault<z.ZodString>;
415
409
  opacity: z.ZodDefault<z.ZodNumber>;
416
410
  background: z.ZodOptional<z.ZodString>;
417
- textDecoration: z.ZodDefault<z.ZodEnum<{
418
- none: "none";
419
- underline: "underline";
420
- "line-through": "line-through";
421
- }>>;
422
411
  }, z.core.$strip>>;
423
412
  style: z.ZodOptional<z.ZodObject<{
424
413
  wordSpacing: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>, z.ZodString]>>>;
@@ -499,39 +488,38 @@ declare const CanvasRichCaptionAssetSchema: z.ZodObject<{
499
488
  "line-through": "line-through";
500
489
  }>>;
501
490
  }, z.core.$strip>>;
502
- stroke: z.ZodOptional<z.ZodObject<{
491
+ stroke: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
503
492
  width: z.ZodOptional<z.ZodNumber>;
504
493
  color: z.ZodOptional<z.ZodString>;
505
494
  opacity: z.ZodOptional<z.ZodNumber>;
506
- }, z.core.$strip>>;
507
- shadow: z.ZodOptional<z.ZodObject<{
495
+ }, z.core.$strip>, z.ZodLiteral<"none">]>>;
496
+ shadow: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
508
497
  offsetX: z.ZodOptional<z.ZodNumber>;
509
498
  offsetY: z.ZodOptional<z.ZodNumber>;
510
499
  blur: z.ZodOptional<z.ZodNumber>;
511
500
  color: z.ZodOptional<z.ZodString>;
512
501
  opacity: z.ZodOptional<z.ZodNumber>;
513
- }, z.core.$strip>>;
502
+ }, z.core.$strip>, z.ZodLiteral<"none">]>>;
514
503
  scale: z.ZodDefault<z.ZodNumber>;
515
504
  }, z.core.$strict>>;
516
505
  wordAnimation: z.ZodOptional<z.ZodObject<{
517
506
  style: z.ZodDefault<z.ZodEnum<{
518
507
  typewriter: "typewriter";
519
508
  none: "none";
509
+ pop: "pop";
520
510
  karaoke: "karaoke";
521
511
  highlight: "highlight";
522
- pop: "pop";
523
512
  fade: "fade";
524
513
  slide: "slide";
525
514
  bounce: "bounce";
526
515
  }>>;
527
- speed: z.ZodDefault<z.ZodNumber>;
528
516
  direction: z.ZodDefault<z.ZodEnum<{
529
517
  right: "right";
530
518
  left: "left";
531
519
  up: "up";
532
520
  down: "down";
533
521
  }>>;
534
- }, z.core.$strict>>;
522
+ }, z.core.$strip>>;
535
523
  pauseThreshold: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
536
524
  customFonts: z.ZodOptional<z.ZodArray<z.ZodObject<{
537
525
  src: z.ZodString;
@@ -663,7 +651,6 @@ interface CaptionLayoutConfig {
663
651
  fontFamily: string;
664
652
  fontWeight: string | number;
665
653
  letterSpacing: number;
666
- wordSpacing: number;
667
654
  lineHeight: number;
668
655
  textTransform: "none" | "uppercase" | "lowercase" | "capitalize";
669
656
  pauseThreshold: number;
@@ -745,7 +732,6 @@ type AnimationStyle = "karaoke" | "highlight" | "pop" | "fade" | "slide" | "boun
745
732
  type AnimationDirection = "left" | "right" | "up" | "down";
746
733
  interface WordAnimationConfig {
747
734
  style: AnimationStyle;
748
- speed: number;
749
735
  direction: AnimationDirection;
750
736
  }
751
737
  interface WordAnimationState {
@@ -1183,7 +1169,7 @@ interface FrameSchedule {
1183
1169
  uniqueFrameCount: number;
1184
1170
  skipRatio: number;
1185
1171
  }
1186
- declare function createFrameSchedule(layout: CaptionLayout, durationMs: number, fps: number, animationStyle?: AnimationStyle, speed?: number): FrameSchedule;
1172
+ declare function createFrameSchedule(layout: CaptionLayout, durationMs: number, fps: number, animationStyle?: AnimationStyle): FrameSchedule;
1187
1173
 
1188
1174
  interface RichCaptionRendererOptions {
1189
1175
  width: number;
@@ -1252,7 +1238,6 @@ declare class RichCaptionRenderer {
1252
1238
  private mapVerticalAlign;
1253
1239
  private mapHorizontalAlign;
1254
1240
  private extractAnimationStyle;
1255
- private extractAnimationSpeed;
1256
1241
  private logProgress;
1257
1242
  private logCompletion;
1258
1243
  private checkMemoryUsage;
@@ -18,8 +18,7 @@ import {
18
18
  svgShadowSchema,
19
19
  svgTransformSchema,
20
20
  svgGradientStopSchema,
21
- richCaptionActiveSchema as baseCaptionActiveSchema,
22
- richCaptionWordAnimationSchema as baseCaptionWordAnimationSchema
21
+ richCaptionActiveSchema as baseCaptionActiveSchema
23
22
  } from "@shotstack/schemas/zod";
24
23
 
25
24
  // src/config/canvas-constants.ts
@@ -188,8 +187,7 @@ var richCaptionFontSchema = z.object({
188
187
  weight: z.union([z.string(), z.number()]).default("400"),
189
188
  color: z.string().regex(HEX6).default("#ffffff"),
190
189
  opacity: z.number().min(0).max(1).default(1),
191
- background: z.string().regex(HEX6).optional(),
192
- textDecoration: z.enum(["none", "underline", "line-through"]).default("none")
190
+ background: z.string().regex(HEX6).optional()
193
191
  });
194
192
  var richCaptionActiveSchema = baseCaptionActiveSchema.extend({
195
193
  font: z.object({
@@ -198,23 +196,28 @@ var richCaptionActiveSchema = baseCaptionActiveSchema.extend({
198
196
  opacity: z.number().min(0).max(1).optional(),
199
197
  textDecoration: z.enum(["none", "underline", "line-through"]).optional()
200
198
  }).optional(),
201
- stroke: z.object({
202
- width: z.number().min(0).optional(),
203
- color: z.string().regex(HEX6).optional(),
204
- opacity: z.number().min(0).max(1).optional()
205
- }).optional(),
206
- shadow: z.object({
207
- offsetX: z.number().optional(),
208
- offsetY: z.number().optional(),
209
- blur: z.number().min(0).optional(),
210
- color: z.string().regex(HEX6).optional(),
211
- opacity: z.number().min(0).max(1).optional()
212
- }).optional(),
199
+ stroke: z.union([
200
+ z.object({
201
+ width: z.number().min(0).optional(),
202
+ color: z.string().regex(HEX6).optional(),
203
+ opacity: z.number().min(0).max(1).optional()
204
+ }),
205
+ z.literal("none")
206
+ ]).optional(),
207
+ shadow: z.union([
208
+ z.object({
209
+ offsetX: z.number().optional(),
210
+ offsetY: z.number().optional(),
211
+ blur: z.number().min(0).optional(),
212
+ color: z.string().regex(HEX6).optional(),
213
+ opacity: z.number().min(0).max(1).optional()
214
+ }),
215
+ z.literal("none")
216
+ ]).optional(),
213
217
  scale: z.number().min(0.5).max(2).default(1)
214
218
  });
215
- var richCaptionWordAnimationSchema = baseCaptionWordAnimationSchema.extend({
219
+ var richCaptionWordAnimationSchema = z.object({
216
220
  style: z.enum(["karaoke", "highlight", "pop", "fade", "slide", "bounce", "typewriter", "none"]).default("highlight"),
217
- speed: z.number().min(0.5).max(2).default(1),
218
221
  direction: z.enum(["left", "right", "up", "down"]).default("up")
219
222
  });
220
223
  var richCaptionAssetSchema = z.object({
@@ -1131,7 +1134,7 @@ var LayoutEngine = class {
1131
1134
  }
1132
1135
  async layout(params) {
1133
1136
  try {
1134
- const { textTransform, desc, fontSize, letterSpacing, width, emojiFallback } = params;
1137
+ const { textTransform, desc, fontSize, letterSpacing, wordSpacing = 0, width, emojiFallback } = params;
1135
1138
  const input = this.transformText(params.text, textTransform);
1136
1139
  if (!input || input.length === 0) {
1137
1140
  return [];
@@ -1161,9 +1164,10 @@ var LayoutEngine = class {
1161
1164
  char = String.fromCodePoint(codePoint);
1162
1165
  }
1163
1166
  }
1167
+ const isSpace = char === " ";
1164
1168
  return {
1165
1169
  id: g.g,
1166
- xAdvance: g.ax * scale + letterSpacing,
1170
+ xAdvance: g.ax * scale + letterSpacing + (isSpace ? wordSpacing : 0),
1167
1171
  xOffset: g.dx * scale,
1168
1172
  yOffset: -g.dy * scale,
1169
1173
  cluster: g.cl,
@@ -2440,10 +2444,10 @@ var CaptionLayoutEngine = class {
2440
2444
  let spaceWidth;
2441
2445
  if (config.measureTextWidth) {
2442
2446
  const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
2443
- spaceWidth = config.measureTextWidth(" ", fontString) + config.wordSpacing;
2447
+ spaceWidth = config.measureTextWidth(" ", fontString);
2444
2448
  } else {
2445
2449
  const spaceWord = await this.measureWord(" ", measurementConfig);
2446
- spaceWidth = spaceWord.width + config.wordSpacing;
2450
+ spaceWidth = spaceWord.width;
2447
2451
  }
2448
2452
  const groups = wordGroups.flatMap((indices) => {
2449
2453
  const groupWidths = indices.map((i) => store.widths[i]);
@@ -2642,12 +2646,8 @@ function calculateWordProgress(ctx) {
2642
2646
  function isWordActive(ctx) {
2643
2647
  return ctx.currentTime >= ctx.wordStart && ctx.currentTime < ctx.wordEnd;
2644
2648
  }
2645
- function calculateKaraokeState(ctx, speed) {
2649
+ function calculateKaraokeState(ctx) {
2646
2650
  const isActive = isWordActive(ctx);
2647
- const wordDuration = ctx.wordEnd - ctx.wordStart;
2648
- const adjustedDuration = wordDuration / speed;
2649
- const adjustedEnd = ctx.wordStart + adjustedDuration;
2650
- const adjustedCtx = { ...ctx, wordEnd: adjustedEnd };
2651
2651
  if (ctx.currentTime < ctx.wordStart) {
2652
2652
  return {
2653
2653
  fillProgress: 0,
@@ -2655,7 +2655,7 @@ function calculateKaraokeState(ctx, speed) {
2655
2655
  opacity: 1
2656
2656
  };
2657
2657
  }
2658
- if (ctx.currentTime >= adjustedEnd) {
2658
+ if (ctx.currentTime >= ctx.wordEnd) {
2659
2659
  return {
2660
2660
  fillProgress: 1,
2661
2661
  isActive: false,
@@ -2663,7 +2663,7 @@ function calculateKaraokeState(ctx, speed) {
2663
2663
  };
2664
2664
  }
2665
2665
  return {
2666
- fillProgress: calculateWordProgress(adjustedCtx),
2666
+ fillProgress: calculateWordProgress(ctx),
2667
2667
  isActive,
2668
2668
  opacity: 1
2669
2669
  };
@@ -2676,7 +2676,7 @@ function calculateHighlightState(ctx) {
2676
2676
  opacity: 1
2677
2677
  };
2678
2678
  }
2679
- function calculatePopState(ctx, activeScale, speed) {
2679
+ function calculatePopState(ctx, activeScale) {
2680
2680
  if (ctx.currentTime < ctx.wordStart) {
2681
2681
  return {
2682
2682
  scale: 0.5,
@@ -2685,9 +2685,7 @@ function calculatePopState(ctx, activeScale, speed) {
2685
2685
  fillProgress: 0
2686
2686
  };
2687
2687
  }
2688
- const adjustedDuration = ctx.animationDuration / speed;
2689
- const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
2690
- const progress = calculateAnimationProgress(adjustedCtx);
2688
+ const progress = calculateAnimationProgress(ctx);
2691
2689
  const easedProgress = easeOutBack(progress);
2692
2690
  const startScale = 0.5;
2693
2691
  const isActive = isWordActive(ctx);
@@ -2700,7 +2698,7 @@ function calculatePopState(ctx, activeScale, speed) {
2700
2698
  fillProgress: isActive ? 1 : 0
2701
2699
  };
2702
2700
  }
2703
- function calculateFadeState(ctx, speed) {
2701
+ function calculateFadeState(ctx) {
2704
2702
  if (ctx.currentTime < ctx.wordStart) {
2705
2703
  return {
2706
2704
  opacity: 0,
@@ -2708,9 +2706,7 @@ function calculateFadeState(ctx, speed) {
2708
2706
  fillProgress: 0
2709
2707
  };
2710
2708
  }
2711
- const adjustedDuration = ctx.animationDuration / speed;
2712
- const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
2713
- const progress = calculateAnimationProgress(adjustedCtx);
2709
+ const progress = calculateAnimationProgress(ctx);
2714
2710
  const easedProgress = easeInOutQuad(progress);
2715
2711
  const isActive = isWordActive(ctx);
2716
2712
  return {
@@ -2719,7 +2715,7 @@ function calculateFadeState(ctx, speed) {
2719
2715
  fillProgress: isActive ? 1 : 0
2720
2716
  };
2721
2717
  }
2722
- function calculateSlideState(ctx, direction, speed, fontSize) {
2718
+ function calculateSlideState(ctx, direction, fontSize) {
2723
2719
  const slideDistance = fontSize * 1.5;
2724
2720
  if (ctx.currentTime < ctx.wordStart) {
2725
2721
  const offset2 = getDirectionOffset(direction, slideDistance);
@@ -2731,9 +2727,7 @@ function calculateSlideState(ctx, direction, speed, fontSize) {
2731
2727
  fillProgress: 0
2732
2728
  };
2733
2729
  }
2734
- const adjustedDuration = ctx.animationDuration / speed;
2735
- const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
2736
- const progress = calculateAnimationProgress(adjustedCtx);
2730
+ const progress = calculateAnimationProgress(ctx);
2737
2731
  const easedProgress = easeOutCirc(progress);
2738
2732
  const offset = getDirectionOffset(direction, slideDistance);
2739
2733
  const translateX = offset.x * (1 - easedProgress);
@@ -2759,7 +2753,7 @@ function getDirectionOffset(direction, distance) {
2759
2753
  return { x: 0, y: -distance };
2760
2754
  }
2761
2755
  }
2762
- function calculateBounceState(ctx, speed, fontSize) {
2756
+ function calculateBounceState(ctx, fontSize) {
2763
2757
  const bounceDistance = fontSize * 0.8;
2764
2758
  if (ctx.currentTime < ctx.wordStart) {
2765
2759
  return {
@@ -2769,9 +2763,7 @@ function calculateBounceState(ctx, speed, fontSize) {
2769
2763
  fillProgress: 0
2770
2764
  };
2771
2765
  }
2772
- const adjustedDuration = ctx.animationDuration / speed;
2773
- const adjustedCtx = { ...ctx, animationDuration: adjustedDuration };
2774
- const progress = calculateAnimationProgress(adjustedCtx);
2766
+ const progress = calculateAnimationProgress(ctx);
2775
2767
  const easedProgress = easeOutBounce(progress);
2776
2768
  const isActive = isWordActive(ctx);
2777
2769
  return {
@@ -2781,11 +2773,7 @@ function calculateBounceState(ctx, speed, fontSize) {
2781
2773
  fillProgress: isActive ? 1 : 0
2782
2774
  };
2783
2775
  }
2784
- function calculateTypewriterState(ctx, charCount, speed) {
2785
- const wordDuration = ctx.wordEnd - ctx.wordStart;
2786
- const adjustedDuration = wordDuration / speed;
2787
- const adjustedEnd = ctx.wordStart + adjustedDuration;
2788
- const adjustedCtx = { ...ctx, wordEnd: adjustedEnd };
2776
+ function calculateTypewriterState(ctx, charCount) {
2789
2777
  if (ctx.currentTime < ctx.wordStart) {
2790
2778
  return {
2791
2779
  visibleCharacters: 0,
@@ -2794,7 +2782,7 @@ function calculateTypewriterState(ctx, charCount, speed) {
2794
2782
  fillProgress: 0
2795
2783
  };
2796
2784
  }
2797
- if (ctx.currentTime >= adjustedEnd) {
2785
+ if (ctx.currentTime >= ctx.wordEnd) {
2798
2786
  return {
2799
2787
  visibleCharacters: charCount,
2800
2788
  opacity: 1,
@@ -2802,7 +2790,7 @@ function calculateTypewriterState(ctx, charCount, speed) {
2802
2790
  fillProgress: 0
2803
2791
  };
2804
2792
  }
2805
- const progress = calculateWordProgress(adjustedCtx);
2793
+ const progress = calculateWordProgress(ctx);
2806
2794
  const visibleCharacters = Math.ceil(progress * charCount);
2807
2795
  const isActive = isWordActive(ctx);
2808
2796
  return {
@@ -2820,7 +2808,6 @@ function calculateNoneState(_ctx) {
2820
2808
  };
2821
2809
  }
2822
2810
  function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
2823
- const safeSpeed = config.speed > 0 ? config.speed : 1;
2824
2811
  const ctx = {
2825
2812
  wordStart,
2826
2813
  wordEnd,
@@ -2831,27 +2818,27 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
2831
2818
  let partialState;
2832
2819
  switch (config.style) {
2833
2820
  case "karaoke":
2834
- partialState = calculateKaraokeState(ctx, safeSpeed);
2821
+ partialState = calculateKaraokeState(ctx);
2835
2822
  break;
2836
2823
  case "highlight":
2837
2824
  partialState = calculateHighlightState(ctx);
2838
2825
  break;
2839
2826
  case "pop":
2840
- partialState = calculatePopState(ctx, activeScale, safeSpeed);
2827
+ partialState = calculatePopState(ctx, activeScale);
2841
2828
  break;
2842
2829
  case "fade":
2843
- partialState = calculateFadeState(ctx, safeSpeed);
2830
+ partialState = calculateFadeState(ctx);
2844
2831
  break;
2845
2832
  case "slide": {
2846
2833
  const slideDir = mirrorAnimationDirection(config.direction, isRTL);
2847
- partialState = calculateSlideState(ctx, slideDir, config.speed, fontSize);
2834
+ partialState = calculateSlideState(ctx, slideDir, fontSize);
2848
2835
  break;
2849
2836
  }
2850
2837
  case "bounce":
2851
- partialState = calculateBounceState(ctx, safeSpeed, fontSize);
2838
+ partialState = calculateBounceState(ctx, fontSize);
2852
2839
  break;
2853
2840
  case "typewriter":
2854
- partialState = calculateTypewriterState(ctx, charCount, safeSpeed);
2841
+ partialState = calculateTypewriterState(ctx, charCount);
2855
2842
  break;
2856
2843
  case "none":
2857
2844
  default:
@@ -2884,7 +2871,6 @@ function calculateAnimationStatesForGroup(words, currentTime, config, activeScal
2884
2871
  function getDefaultAnimationConfig() {
2885
2872
  return {
2886
2873
  style: "highlight",
2887
- speed: 1,
2888
2874
  direction: "up"
2889
2875
  };
2890
2876
  }
@@ -2925,18 +2911,17 @@ function extractFontConfig(asset) {
2925
2911
  function extractStrokeConfig(asset, isActive) {
2926
2912
  const baseStroke = asset.stroke;
2927
2913
  const activeStroke = asset.active?.stroke;
2928
- if (!baseStroke && !activeStroke) {
2929
- return void 0;
2930
- }
2931
2914
  if (isActive) {
2932
- if (!activeStroke) {
2915
+ if (activeStroke === "none") {
2933
2916
  return void 0;
2934
2917
  }
2935
- return {
2936
- width: activeStroke.width ?? baseStroke?.width ?? 0,
2937
- color: activeStroke.color ?? baseStroke?.color ?? "#000000",
2938
- opacity: activeStroke.opacity ?? baseStroke?.opacity ?? 1
2939
- };
2918
+ if (activeStroke && typeof activeStroke === "object") {
2919
+ return {
2920
+ width: activeStroke.width ?? baseStroke?.width ?? 0,
2921
+ color: activeStroke.color ?? baseStroke?.color ?? "#000000",
2922
+ opacity: activeStroke.opacity ?? baseStroke?.opacity ?? 1
2923
+ };
2924
+ }
2940
2925
  }
2941
2926
  if (baseStroke) {
2942
2927
  return {
@@ -2950,20 +2935,19 @@ function extractStrokeConfig(asset, isActive) {
2950
2935
  function extractShadowConfig(asset, isActive) {
2951
2936
  const baseShadow = asset.shadow;
2952
2937
  const activeShadow = asset.active?.shadow;
2953
- if (!baseShadow && !activeShadow) {
2954
- return void 0;
2955
- }
2956
2938
  if (isActive) {
2957
- if (!activeShadow) {
2939
+ if (activeShadow === "none") {
2958
2940
  return void 0;
2959
2941
  }
2960
- return {
2961
- offsetX: activeShadow.offsetX ?? baseShadow?.offsetX ?? 0,
2962
- offsetY: activeShadow.offsetY ?? baseShadow?.offsetY ?? 0,
2963
- blur: activeShadow.blur ?? baseShadow?.blur ?? 0,
2964
- color: activeShadow.color ?? baseShadow?.color ?? "#000000",
2965
- opacity: activeShadow.opacity ?? baseShadow?.opacity ?? 0.5
2966
- };
2942
+ if (activeShadow && typeof activeShadow === "object") {
2943
+ return {
2944
+ offsetX: activeShadow.offsetX ?? baseShadow?.offsetX ?? 0,
2945
+ offsetY: activeShadow.offsetY ?? baseShadow?.offsetY ?? 0,
2946
+ blur: activeShadow.blur ?? baseShadow?.blur ?? 0,
2947
+ color: activeShadow.color ?? baseShadow?.color ?? "#000000",
2948
+ opacity: activeShadow.opacity ?? baseShadow?.opacity ?? 0.5
2949
+ };
2950
+ }
2967
2951
  }
2968
2952
  if (baseShadow) {
2969
2953
  return {
@@ -2979,7 +2963,12 @@ function extractShadowConfig(asset, isActive) {
2979
2963
  function extractBackgroundConfig(asset, isActive, fontSize) {
2980
2964
  const fontBackground = asset.font?.background;
2981
2965
  const activeBackground = asset.active?.font?.background;
2982
- const bgColor = isActive && activeBackground ? activeBackground : fontBackground;
2966
+ let bgColor;
2967
+ if (isActive) {
2968
+ bgColor = activeBackground ?? fontBackground;
2969
+ } else {
2970
+ bgColor = fontBackground;
2971
+ }
2983
2972
  if (!bgColor) {
2984
2973
  return void 0;
2985
2974
  }
@@ -3030,7 +3019,7 @@ function extractCaptionBorder(asset) {
3030
3019
  };
3031
3020
  }
3032
3021
  function extractTextDecoration(asset, isActive) {
3033
- const baseDecoration = asset.font?.textDecoration;
3022
+ const baseDecoration = asset.style?.textDecoration;
3034
3023
  const activeDecoration = asset.active?.font?.textDecoration;
3035
3024
  if (isActive && activeDecoration !== void 0) {
3036
3025
  return activeDecoration === "none" ? void 0 : activeDecoration;
@@ -3047,7 +3036,6 @@ function extractAnimationConfig(asset) {
3047
3036
  }
3048
3037
  return {
3049
3038
  style: wordAnim.style ?? "highlight",
3050
- speed: wordAnim.speed ?? 1,
3051
3039
  direction: wordAnim.direction ?? "up"
3052
3040
  };
3053
3041
  }
@@ -5094,11 +5082,14 @@ function extractSvgDimensions(svgString) {
5094
5082
  }
5095
5083
 
5096
5084
  // src/core/canvas-text-measurer.ts
5097
- async function createCanvasTextMeasurer() {
5085
+ async function createCanvasTextMeasurer(letterSpacing) {
5098
5086
  const canvasMod = await import("canvas");
5099
5087
  const canvas = canvasMod.createCanvas(1, 1);
5100
5088
  const ctx = canvas.getContext("2d");
5101
5089
  ctx.textBaseline = "alphabetic";
5090
+ if (letterSpacing) {
5091
+ ctx.letterSpacing = `${letterSpacing}px`;
5092
+ }
5102
5093
  let lastFont = "";
5103
5094
  return (text, font) => {
5104
5095
  if (font !== lastFont) {
@@ -5467,7 +5458,7 @@ function findActiveWordIndex(store, groupWordIndices, timeMs) {
5467
5458
  }
5468
5459
  return -1;
5469
5460
  }
5470
- function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle, speed) {
5461
+ function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle) {
5471
5462
  if (groupWordIndices.length === 0) {
5472
5463
  return "idle";
5473
5464
  }
@@ -5484,7 +5475,7 @@ function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle, spee
5484
5475
  return "after";
5485
5476
  }
5486
5477
  if (TRANSITION_ANIMATION_STYLES.has(animationStyle)) {
5487
- const transitionDurationMs = (ANIMATION_DURATION_MS[animationStyle] ?? 200) / speed;
5478
+ const transitionDurationMs = ANIMATION_DURATION_MS[animationStyle] ?? 200;
5488
5479
  for (const idx of groupWordIndices) {
5489
5480
  const wordStart = store.startTimes[idx];
5490
5481
  if (timeMs >= wordStart && timeMs < wordStart + transitionDurationMs) {
@@ -5506,7 +5497,7 @@ function getAnimationPhase(store, groupWordIndices, timeMs, animationStyle, spee
5506
5497
  }
5507
5498
  return "before";
5508
5499
  }
5509
- function computeStateSignature(layout, timeMs, animationStyle, speed) {
5500
+ function computeStateSignature(layout, timeMs, animationStyle) {
5510
5501
  const groupIndex = findGroupIndexAtTime(layout.groups, timeMs);
5511
5502
  if (groupIndex === -1) {
5512
5503
  return { groupIndex: -1, activeWordIndex: -1, animationPhase: "idle" };
@@ -5517,21 +5508,20 @@ function computeStateSignature(layout, timeMs, animationStyle, speed) {
5517
5508
  layout.store,
5518
5509
  group.wordIndices,
5519
5510
  timeMs,
5520
- animationStyle,
5521
- speed
5511
+ animationStyle
5522
5512
  );
5523
5513
  return { groupIndex, activeWordIndex, animationPhase };
5524
5514
  }
5525
5515
  function signaturesMatch(a, b) {
5526
5516
  return a.groupIndex === b.groupIndex && a.activeWordIndex === b.activeWordIndex && a.animationPhase === b.animationPhase;
5527
5517
  }
5528
- function createFrameSchedule(layout, durationMs, fps, animationStyle = "highlight", speed = 1) {
5518
+ function createFrameSchedule(layout, durationMs, fps, animationStyle = "highlight") {
5529
5519
  const totalFrames = Math.max(2, Math.round(durationMs / 1e3 * fps) + 1);
5530
5520
  const renderFrames = [];
5531
5521
  let previousSignature = null;
5532
5522
  for (let frame = 0; frame < totalFrames; frame++) {
5533
5523
  const timeMs = frame / (totalFrames - 1) * durationMs;
5534
- const signature = computeStateSignature(layout, timeMs, animationStyle, speed);
5524
+ const signature = computeStateSignature(layout, timeMs, animationStyle);
5535
5525
  const isAnimating = signature.animationPhase === "animating";
5536
5526
  if (isAnimating || previousSignature === null || !signaturesMatch(signature, previousSignature)) {
5537
5527
  renderFrames.push({
@@ -5878,7 +5868,7 @@ var RichCaptionRenderer = class {
5878
5868
  }
5879
5869
  const font = asset.font;
5880
5870
  const style = asset.style;
5881
- const measureTextWidth = await createCanvasTextMeasurer();
5871
+ const measureTextWidth = await createCanvasTextMeasurer(style?.letterSpacing ?? 0);
5882
5872
  const fontSize = font?.size ?? 24;
5883
5873
  const lineHeight = style?.lineHeight ?? 1.2;
5884
5874
  const padding = extractCaptionPadding(asset);
@@ -5899,7 +5889,6 @@ var RichCaptionRenderer = class {
5899
5889
  fontFamily: font?.family ?? "Roboto",
5900
5890
  fontWeight: String(font?.weight ?? "400"),
5901
5891
  letterSpacing: style?.letterSpacing ?? 0,
5902
- wordSpacing: typeof style?.wordSpacing === "number" ? style.wordSpacing : 0,
5903
5892
  lineHeight,
5904
5893
  textTransform: style?.textTransform ?? "none",
5905
5894
  pauseThreshold: this.currentAsset?.pauseThreshold ?? 500,
@@ -5933,14 +5922,12 @@ var RichCaptionRenderer = class {
5933
5922
  throw new Error("No asset loaded. Call loadAsset() first.");
5934
5923
  }
5935
5924
  const animationStyle = this.extractAnimationStyle();
5936
- const animationSpeed = this.extractAnimationSpeed();
5937
5925
  const durationMs = duration * 1e3;
5938
5926
  const schedule = createFrameSchedule(
5939
5927
  this.currentLayout,
5940
5928
  durationMs,
5941
5929
  this.fps,
5942
- animationStyle,
5943
- animationSpeed
5930
+ animationStyle
5944
5931
  );
5945
5932
  const bgColor = options?.bgColor;
5946
5933
  const hasAlpha = !bgColor;
@@ -6101,13 +6088,11 @@ var RichCaptionRenderer = class {
6101
6088
  throw new Error("No asset loaded. Call loadAsset() first.");
6102
6089
  }
6103
6090
  const animationStyle = this.extractAnimationStyle();
6104
- const animationSpeed = this.extractAnimationSpeed();
6105
6091
  return createFrameSchedule(
6106
6092
  this.currentLayout,
6107
6093
  duration * 1e3,
6108
6094
  this.fps,
6109
- animationStyle,
6110
- animationSpeed
6095
+ animationStyle
6111
6096
  );
6112
6097
  }
6113
6098
  getStats() {
@@ -6145,10 +6130,6 @@ var RichCaptionRenderer = class {
6145
6130
  const wordAnim = this.currentAsset?.wordAnimation;
6146
6131
  return wordAnim?.style ?? "highlight";
6147
6132
  }
6148
- extractAnimationSpeed() {
6149
- const wordAnim = this.currentAsset?.wordAnimation;
6150
- return wordAnim?.speed ?? 1;
6151
- }
6152
6133
  logProgress(pct, framesProcessed, totalFrames, uniqueProcessed, uniqueTotal, fps, eta) {
6153
6134
  if (typeof process !== "undefined" && process.stderr) {
6154
6135
  process.stderr.write(
@@ -6461,6 +6442,7 @@ async function createTextEngine(opts = {}) {
6461
6442
  text: asset.text,
6462
6443
  width: (asset.width ?? width) - padding2.left - padding2.right,
6463
6444
  letterSpacing: asset.style?.letterSpacing ?? 0,
6445
+ wordSpacing: typeof asset.style?.wordSpacing === "number" ? asset.style.wordSpacing : 0,
6464
6446
  fontSize: main.size,
6465
6447
  lineHeight: asset.style?.lineHeight ?? 1.2,
6466
6448
  desc,