@shotstack/shotstack-canvas 2.1.0 → 2.1.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.
@@ -2842,10 +2842,11 @@ function calculateTypewriterState(ctx, charCount, speed) {
2842
2842
  isActive: isWordActive(ctx)
2843
2843
  };
2844
2844
  }
2845
- function calculateNoneState(ctx) {
2845
+ function calculateNoneState(_ctx) {
2846
2846
  return {
2847
2847
  opacity: 1,
2848
- isActive: isWordActive(ctx)
2848
+ isActive: false,
2849
+ fillProgress: 0
2849
2850
  };
2850
2851
  }
2851
2852
  function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
@@ -2888,7 +2889,7 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
2888
2889
  break;
2889
2890
  }
2890
2891
  const mergedState = { ...baseState, ...partialState };
2891
- if (mergedState.isActive && activeScale !== 1 && config.style !== "pop") {
2892
+ if (mergedState.isActive && activeScale !== 1 && config.style !== "pop" && config.style !== "none") {
2892
2893
  mergedState.scale = activeScale;
2893
2894
  }
2894
2895
  return mergedState;
@@ -2950,7 +2951,10 @@ function extractStrokeConfig(asset, isActive) {
2950
2951
  if (!baseStroke && !activeStroke) {
2951
2952
  return void 0;
2952
2953
  }
2953
- if (isActive && activeStroke) {
2954
+ if (isActive) {
2955
+ if (!activeStroke) {
2956
+ return void 0;
2957
+ }
2954
2958
  return {
2955
2959
  width: activeStroke.width ?? baseStroke?.width ?? 0,
2956
2960
  color: activeStroke.color ?? baseStroke?.color ?? "#000000",
@@ -2966,7 +2970,10 @@ function extractStrokeConfig(asset, isActive) {
2966
2970
  }
2967
2971
  return void 0;
2968
2972
  }
2969
- function extractShadowConfig(asset) {
2973
+ function extractShadowConfig(asset, isActive) {
2974
+ if (isActive) {
2975
+ return void 0;
2976
+ }
2970
2977
  const shadow = asset.shadow;
2971
2978
  if (!shadow) {
2972
2979
  return void 0;
@@ -3073,27 +3080,10 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
3073
3080
  visibleCharacters: animState.visibleCharacters,
3074
3081
  letterSpacing: fontConfig.letterSpacing > 0 ? fontConfig.letterSpacing : void 0,
3075
3082
  stroke: extractStrokeConfig(asset, isActive),
3076
- shadow: extractShadowConfig(asset),
3083
+ shadow: extractShadowConfig(asset, isActive),
3077
3084
  background: extractBackgroundConfig(asset, isActive, fontConfig.size)
3078
3085
  };
3079
3086
  }
3080
- function calculateGroupBounds(activeGroup, padding, frameWidth, activeScale, fontSize) {
3081
- let minY = Infinity;
3082
- let maxY = -Infinity;
3083
- for (const line of activeGroup.lines) {
3084
- const lineY = line.y - line.height * ASCENT_RATIO;
3085
- const lineBottom = line.y + line.height * DESCENT_RATIO;
3086
- if (lineY < minY) minY = lineY;
3087
- if (lineBottom > maxY) maxY = lineBottom;
3088
- }
3089
- const scaleExpansion = activeScale > 1 ? (activeScale - 1) * fontSize : 0;
3090
- return {
3091
- bgX: 0,
3092
- bgY: minY - padding.top - scaleExpansion,
3093
- bgWidth: frameWidth,
3094
- bgHeight: maxY - minY + padding.top + padding.bottom + scaleExpansion * 2
3095
- };
3096
- }
3097
3087
  function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, config) {
3098
3088
  if (layout.store.length === 0) {
3099
3089
  return [];
@@ -3113,48 +3103,35 @@ function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, co
3113
3103
  fontConfig.size
3114
3104
  );
3115
3105
  const ops = [];
3116
- const activeGroup = layout.groups.find(
3117
- (g) => frameTimeMs >= g.startTime && frameTimeMs <= g.endTime
3118
- );
3119
- if (activeGroup && activeGroup.lines.length > 0) {
3120
- const padding = extractCaptionPadding(asset);
3121
- const { bgX, bgY, bgWidth, bgHeight } = calculateGroupBounds(
3122
- activeGroup,
3123
- padding,
3124
- config.frameWidth,
3125
- activeScale,
3126
- fontConfig.size
3127
- );
3128
- const captionBg = extractCaptionBackground(asset);
3129
- if (captionBg) {
3130
- ops.push({
3131
- op: "DrawCaptionBackground",
3132
- x: bgX,
3133
- y: bgY,
3134
- width: bgWidth,
3135
- height: bgHeight,
3136
- color: captionBg.color,
3137
- opacity: captionBg.opacity,
3138
- borderRadius: captionBg.borderRadius
3139
- });
3140
- }
3141
- const borderConfig = extractCaptionBorder(asset);
3142
- if (borderConfig) {
3143
- const halfBorder = borderConfig.width / 2;
3144
- ops.push({
3145
- op: "RectangleStroke",
3146
- x: bgX + halfBorder,
3147
- y: bgY + halfBorder,
3148
- width: bgWidth - borderConfig.width,
3149
- height: bgHeight - borderConfig.width,
3150
- stroke: {
3151
- width: borderConfig.width,
3152
- color: borderConfig.color,
3153
- opacity: borderConfig.opacity
3154
- },
3155
- borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
3156
- });
3157
- }
3106
+ const captionBg = extractCaptionBackground(asset);
3107
+ if (captionBg) {
3108
+ ops.push({
3109
+ op: "DrawCaptionBackground",
3110
+ x: 0,
3111
+ y: 0,
3112
+ width: config.frameWidth,
3113
+ height: config.frameHeight,
3114
+ color: captionBg.color,
3115
+ opacity: captionBg.opacity,
3116
+ borderRadius: captionBg.borderRadius
3117
+ });
3118
+ }
3119
+ const borderConfig = extractCaptionBorder(asset);
3120
+ if (borderConfig) {
3121
+ const halfBorder = borderConfig.width / 2;
3122
+ ops.push({
3123
+ op: "RectangleStroke",
3124
+ x: halfBorder,
3125
+ y: halfBorder,
3126
+ width: config.frameWidth - borderConfig.width,
3127
+ height: config.frameHeight - borderConfig.width,
3128
+ stroke: {
3129
+ width: borderConfig.width,
3130
+ color: borderConfig.color,
3131
+ opacity: borderConfig.opacity
3132
+ },
3133
+ borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
3134
+ });
3158
3135
  }
3159
3136
  const nonActiveOps = [];
3160
3137
  const activeOps = [];
@@ -3501,40 +3478,61 @@ async function createNodePainter(opts) {
3501
3478
  i++;
3502
3479
  }
3503
3480
  renderToBoth((context) => {
3504
- for (const wordOp of captionWordOps) {
3505
- if (!wordOp.background) continue;
3481
+ let bgIdx = 0;
3482
+ while (bgIdx < captionWordOps.length) {
3483
+ const wordOp = captionWordOps[bgIdx];
3484
+ if (!wordOp.background) {
3485
+ bgIdx++;
3486
+ continue;
3487
+ }
3506
3488
  const wordDisplayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
3507
- if (wordDisplayText.length === 0) continue;
3489
+ if (wordDisplayText.length === 0) {
3490
+ bgIdx++;
3491
+ continue;
3492
+ }
3493
+ const mergeGroup = [wordOp];
3494
+ let nextIdx = bgIdx + 1;
3495
+ while (nextIdx < captionWordOps.length) {
3496
+ const nextWord = captionWordOps[nextIdx];
3497
+ if (!nextWord.background) break;
3498
+ const nextDisplay = getVisibleText(nextWord.text, nextWord.visibleCharacters, nextWord.isRTL);
3499
+ if (nextDisplay.length === 0) break;
3500
+ if (Math.round(nextWord.y) !== Math.round(wordOp.y)) break;
3501
+ if (nextWord.background.color !== wordOp.background.color) break;
3502
+ mergeGroup.push(nextWord);
3503
+ nextIdx++;
3504
+ }
3505
+ const firstWord = mergeGroup[0];
3506
+ const lastWord = mergeGroup[mergeGroup.length - 1];
3507
+ const firstBg = firstWord.background;
3508
+ const mergedLeft = firstWord.x + firstWord.transform.translateX;
3509
+ const mergedRight = lastWord.x + lastWord.transform.translateX + lastWord.width;
3510
+ const mergedWidth = mergedRight - mergedLeft;
3508
3511
  context.save();
3509
- const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
3510
- const bgTy = Math.round(wordOp.y + wordOp.transform.translateY);
3512
+ const bgTx = Math.round(mergedLeft);
3513
+ const bgTy = Math.round(firstWord.y + firstWord.transform.translateY);
3511
3514
  context.translate(bgTx, bgTy);
3512
- if (wordOp.transform.scale !== 1) {
3513
- const halfWidth = wordOp.width / 2;
3515
+ if (firstWord.transform.scale !== 1) {
3516
+ const halfWidth = mergedWidth / 2;
3514
3517
  context.translate(halfWidth, 0);
3515
- context.scale(wordOp.transform.scale, wordOp.transform.scale);
3518
+ context.scale(firstWord.transform.scale, firstWord.transform.scale);
3516
3519
  context.translate(-halfWidth, 0);
3517
3520
  }
3518
- context.globalAlpha = wordOp.transform.opacity;
3519
- context.font = `${wordOp.fontWeight} ${wordOp.fontSize}px "${wordOp.fontFamily}"`;
3520
- context.textBaseline = "alphabetic";
3521
- if (wordOp.letterSpacing) {
3522
- context.letterSpacing = `${wordOp.letterSpacing}px`;
3523
- }
3524
- const bgTextWidth = wordOp.width;
3525
- const bgAscent = wordOp.fontSize * ASCENT_RATIO;
3526
- const bgDescent = wordOp.fontSize * DESCENT_RATIO;
3521
+ context.globalAlpha = firstWord.transform.opacity;
3522
+ const bgAscent = firstWord.fontSize * ASCENT_RATIO;
3523
+ const bgDescent = firstWord.fontSize * DESCENT_RATIO;
3527
3524
  const bgTextHeight = bgAscent + bgDescent;
3528
- const bgX = -wordOp.background.padding;
3529
- const bgY = -bgAscent - wordOp.background.padding;
3530
- const bgW = bgTextWidth + wordOp.background.padding * 2;
3531
- const bgH = bgTextHeight + wordOp.background.padding * 2;
3532
- const bgC = parseHex6(wordOp.background.color, wordOp.background.opacity);
3525
+ const bgX = -firstBg.padding;
3526
+ const bgY = -bgAscent - firstBg.padding;
3527
+ const bgW = mergedWidth + firstBg.padding * 2;
3528
+ const bgH = bgTextHeight + firstBg.padding * 2;
3529
+ const bgC = parseHex6(firstBg.color, firstBg.opacity);
3533
3530
  context.fillStyle = `rgba(${bgC.r},${bgC.g},${bgC.b},${bgC.a})`;
3534
3531
  context.beginPath();
3535
- roundRectPath(context, bgX, bgY, bgW, bgH, wordOp.background.borderRadius);
3532
+ roundRectPath(context, bgX, bgY, bgW, bgH, firstBg.borderRadius);
3536
3533
  context.fill();
3537
3534
  context.restore();
3535
+ bgIdx = nextIdx;
3538
3536
  }
3539
3537
  for (const wordOp of captionWordOps) {
3540
3538
  const displayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
@@ -5317,16 +5315,19 @@ var CaptionLayoutEngine = class {
5317
5315
  };
5318
5316
  }).filter((g) => g !== null);
5319
5317
  });
5318
+ const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
5319
+ const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
5320
+ const ASCENT_RATIO2 = 0.8;
5320
5321
  const calculateGroupY = (group) => {
5321
5322
  const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
5322
5323
  switch (config.verticalAlign) {
5323
5324
  case "top":
5324
- return config.fontSize * 1.5;
5325
+ return config.padding.top + config.fontSize * ASCENT_RATIO2;
5325
5326
  case "bottom":
5326
- return config.frameHeight - totalHeight - config.fontSize * 0.5;
5327
+ return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO2;
5327
5328
  case "middle":
5328
5329
  default:
5329
- return (config.frameHeight - totalHeight) / 2 + config.fontSize;
5330
+ return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO2;
5330
5331
  }
5331
5332
  };
5332
5333
  const allWordTexts = store.words.slice(0, store.length);
@@ -5334,12 +5335,12 @@ var CaptionLayoutEngine = class {
5334
5335
  const calculateLineX = (lineWidth) => {
5335
5336
  switch (config.horizontalAlign) {
5336
5337
  case "left":
5337
- return config.paddingLeft;
5338
+ return config.padding.left;
5338
5339
  case "right":
5339
- return config.frameWidth - lineWidth - config.paddingLeft;
5340
+ return config.frameWidth - lineWidth - config.padding.right;
5340
5341
  case "center":
5341
5342
  default:
5342
- return (config.frameWidth - lineWidth) / 2;
5343
+ return config.padding.left + (contentWidth - lineWidth) / 2;
5343
5344
  }
5344
5345
  };
5345
5346
  for (const group of groups) {
@@ -6212,7 +6213,7 @@ var RichCaptionRenderer = class {
6212
6213
  maxLines: computedMaxLines,
6213
6214
  verticalAlign,
6214
6215
  horizontalAlign,
6215
- paddingLeft: padding.left,
6216
+ padding,
6216
6217
  fontSize,
6217
6218
  fontFamily: font?.family ?? "Roboto",
6218
6219
  fontWeight: String(font?.weight ?? "400"),
@@ -609,6 +609,12 @@ interface WordTiming {
609
609
  end: number;
610
610
  confidence?: number;
611
611
  }
612
+ interface CaptionPadding {
613
+ top: number;
614
+ right: number;
615
+ bottom: number;
616
+ left: number;
617
+ }
612
618
  interface CaptionLayoutConfig {
613
619
  frameWidth: number;
614
620
  frameHeight: number;
@@ -616,7 +622,7 @@ interface CaptionLayoutConfig {
616
622
  maxLines: number;
617
623
  verticalAlign: "top" | "middle" | "bottom";
618
624
  horizontalAlign: "left" | "center" | "right";
619
- paddingLeft: number;
625
+ padding: CaptionPadding;
620
626
  fontSize: number;
621
627
  fontFamily: string;
622
628
  fontWeight: string | number;
@@ -1342,4 +1348,4 @@ declare function createTextEngine(opts?: {
1342
1348
  destroy(): void;
1343
1349
  }>;
1344
1350
 
1345
- export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };
1351
+ export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type CaptionPadding, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };
@@ -609,6 +609,12 @@ interface WordTiming {
609
609
  end: number;
610
610
  confidence?: number;
611
611
  }
612
+ interface CaptionPadding {
613
+ top: number;
614
+ right: number;
615
+ bottom: number;
616
+ left: number;
617
+ }
612
618
  interface CaptionLayoutConfig {
613
619
  frameWidth: number;
614
620
  frameHeight: number;
@@ -616,7 +622,7 @@ interface CaptionLayoutConfig {
616
622
  maxLines: number;
617
623
  verticalAlign: "top" | "middle" | "bottom";
618
624
  horizontalAlign: "left" | "center" | "right";
619
- paddingLeft: number;
625
+ padding: CaptionPadding;
620
626
  fontSize: number;
621
627
  fontFamily: string;
622
628
  fontWeight: string | number;
@@ -1342,4 +1348,4 @@ declare function createTextEngine(opts?: {
1342
1348
  destroy(): void;
1343
1349
  }>;
1344
1350
 
1345
- export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };
1351
+ export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type CaptionPadding, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, type MoveToCommand, NodeRawEncoder, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, RichCaptionRenderer, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createNodePainter, createNodeRawEncoder, createRichCaptionRenderer, createTextEngine, createVideoEncoder, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, isDrawCaptionWordOp, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };
@@ -2439,10 +2439,11 @@ function calculateTypewriterState(ctx, charCount, speed) {
2439
2439
  isActive: isWordActive(ctx)
2440
2440
  };
2441
2441
  }
2442
- function calculateNoneState(ctx) {
2442
+ function calculateNoneState(_ctx) {
2443
2443
  return {
2444
2444
  opacity: 1,
2445
- isActive: isWordActive(ctx)
2445
+ isActive: false,
2446
+ fillProgress: 0
2446
2447
  };
2447
2448
  }
2448
2449
  function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
@@ -2485,7 +2486,7 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
2485
2486
  break;
2486
2487
  }
2487
2488
  const mergedState = { ...baseState, ...partialState };
2488
- if (mergedState.isActive && activeScale !== 1 && config.style !== "pop") {
2489
+ if (mergedState.isActive && activeScale !== 1 && config.style !== "pop" && config.style !== "none") {
2489
2490
  mergedState.scale = activeScale;
2490
2491
  }
2491
2492
  return mergedState;
@@ -2547,7 +2548,10 @@ function extractStrokeConfig(asset, isActive) {
2547
2548
  if (!baseStroke && !activeStroke) {
2548
2549
  return void 0;
2549
2550
  }
2550
- if (isActive && activeStroke) {
2551
+ if (isActive) {
2552
+ if (!activeStroke) {
2553
+ return void 0;
2554
+ }
2551
2555
  return {
2552
2556
  width: activeStroke.width ?? baseStroke?.width ?? 0,
2553
2557
  color: activeStroke.color ?? baseStroke?.color ?? "#000000",
@@ -2563,7 +2567,10 @@ function extractStrokeConfig(asset, isActive) {
2563
2567
  }
2564
2568
  return void 0;
2565
2569
  }
2566
- function extractShadowConfig(asset) {
2570
+ function extractShadowConfig(asset, isActive) {
2571
+ if (isActive) {
2572
+ return void 0;
2573
+ }
2567
2574
  const shadow = asset.shadow;
2568
2575
  if (!shadow) {
2569
2576
  return void 0;
@@ -2670,27 +2677,10 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
2670
2677
  visibleCharacters: animState.visibleCharacters,
2671
2678
  letterSpacing: fontConfig.letterSpacing > 0 ? fontConfig.letterSpacing : void 0,
2672
2679
  stroke: extractStrokeConfig(asset, isActive),
2673
- shadow: extractShadowConfig(asset),
2680
+ shadow: extractShadowConfig(asset, isActive),
2674
2681
  background: extractBackgroundConfig(asset, isActive, fontConfig.size)
2675
2682
  };
2676
2683
  }
2677
- function calculateGroupBounds(activeGroup, padding, frameWidth, activeScale, fontSize) {
2678
- let minY = Infinity;
2679
- let maxY = -Infinity;
2680
- for (const line of activeGroup.lines) {
2681
- const lineY = line.y - line.height * ASCENT_RATIO;
2682
- const lineBottom = line.y + line.height * DESCENT_RATIO;
2683
- if (lineY < minY) minY = lineY;
2684
- if (lineBottom > maxY) maxY = lineBottom;
2685
- }
2686
- const scaleExpansion = activeScale > 1 ? (activeScale - 1) * fontSize : 0;
2687
- return {
2688
- bgX: 0,
2689
- bgY: minY - padding.top - scaleExpansion,
2690
- bgWidth: frameWidth,
2691
- bgHeight: maxY - minY + padding.top + padding.bottom + scaleExpansion * 2
2692
- };
2693
- }
2694
2684
  function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, config) {
2695
2685
  if (layout.store.length === 0) {
2696
2686
  return [];
@@ -2710,48 +2700,35 @@ function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, co
2710
2700
  fontConfig.size
2711
2701
  );
2712
2702
  const ops = [];
2713
- const activeGroup = layout.groups.find(
2714
- (g) => frameTimeMs >= g.startTime && frameTimeMs <= g.endTime
2715
- );
2716
- if (activeGroup && activeGroup.lines.length > 0) {
2717
- const padding = extractCaptionPadding(asset);
2718
- const { bgX, bgY, bgWidth, bgHeight } = calculateGroupBounds(
2719
- activeGroup,
2720
- padding,
2721
- config.frameWidth,
2722
- activeScale,
2723
- fontConfig.size
2724
- );
2725
- const captionBg = extractCaptionBackground(asset);
2726
- if (captionBg) {
2727
- ops.push({
2728
- op: "DrawCaptionBackground",
2729
- x: bgX,
2730
- y: bgY,
2731
- width: bgWidth,
2732
- height: bgHeight,
2733
- color: captionBg.color,
2734
- opacity: captionBg.opacity,
2735
- borderRadius: captionBg.borderRadius
2736
- });
2737
- }
2738
- const borderConfig = extractCaptionBorder(asset);
2739
- if (borderConfig) {
2740
- const halfBorder = borderConfig.width / 2;
2741
- ops.push({
2742
- op: "RectangleStroke",
2743
- x: bgX + halfBorder,
2744
- y: bgY + halfBorder,
2745
- width: bgWidth - borderConfig.width,
2746
- height: bgHeight - borderConfig.width,
2747
- stroke: {
2748
- width: borderConfig.width,
2749
- color: borderConfig.color,
2750
- opacity: borderConfig.opacity
2751
- },
2752
- borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
2753
- });
2754
- }
2703
+ const captionBg = extractCaptionBackground(asset);
2704
+ if (captionBg) {
2705
+ ops.push({
2706
+ op: "DrawCaptionBackground",
2707
+ x: 0,
2708
+ y: 0,
2709
+ width: config.frameWidth,
2710
+ height: config.frameHeight,
2711
+ color: captionBg.color,
2712
+ opacity: captionBg.opacity,
2713
+ borderRadius: captionBg.borderRadius
2714
+ });
2715
+ }
2716
+ const borderConfig = extractCaptionBorder(asset);
2717
+ if (borderConfig) {
2718
+ const halfBorder = borderConfig.width / 2;
2719
+ ops.push({
2720
+ op: "RectangleStroke",
2721
+ x: halfBorder,
2722
+ y: halfBorder,
2723
+ width: config.frameWidth - borderConfig.width,
2724
+ height: config.frameHeight - borderConfig.width,
2725
+ stroke: {
2726
+ width: borderConfig.width,
2727
+ color: borderConfig.color,
2728
+ opacity: borderConfig.opacity
2729
+ },
2730
+ borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
2731
+ });
2755
2732
  }
2756
2733
  const nonActiveOps = [];
2757
2734
  const activeOps = [];
@@ -3098,40 +3075,61 @@ async function createNodePainter(opts) {
3098
3075
  i++;
3099
3076
  }
3100
3077
  renderToBoth((context) => {
3101
- for (const wordOp of captionWordOps) {
3102
- if (!wordOp.background) continue;
3078
+ let bgIdx = 0;
3079
+ while (bgIdx < captionWordOps.length) {
3080
+ const wordOp = captionWordOps[bgIdx];
3081
+ if (!wordOp.background) {
3082
+ bgIdx++;
3083
+ continue;
3084
+ }
3103
3085
  const wordDisplayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
3104
- if (wordDisplayText.length === 0) continue;
3086
+ if (wordDisplayText.length === 0) {
3087
+ bgIdx++;
3088
+ continue;
3089
+ }
3090
+ const mergeGroup = [wordOp];
3091
+ let nextIdx = bgIdx + 1;
3092
+ while (nextIdx < captionWordOps.length) {
3093
+ const nextWord = captionWordOps[nextIdx];
3094
+ if (!nextWord.background) break;
3095
+ const nextDisplay = getVisibleText(nextWord.text, nextWord.visibleCharacters, nextWord.isRTL);
3096
+ if (nextDisplay.length === 0) break;
3097
+ if (Math.round(nextWord.y) !== Math.round(wordOp.y)) break;
3098
+ if (nextWord.background.color !== wordOp.background.color) break;
3099
+ mergeGroup.push(nextWord);
3100
+ nextIdx++;
3101
+ }
3102
+ const firstWord = mergeGroup[0];
3103
+ const lastWord = mergeGroup[mergeGroup.length - 1];
3104
+ const firstBg = firstWord.background;
3105
+ const mergedLeft = firstWord.x + firstWord.transform.translateX;
3106
+ const mergedRight = lastWord.x + lastWord.transform.translateX + lastWord.width;
3107
+ const mergedWidth = mergedRight - mergedLeft;
3105
3108
  context.save();
3106
- const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
3107
- const bgTy = Math.round(wordOp.y + wordOp.transform.translateY);
3109
+ const bgTx = Math.round(mergedLeft);
3110
+ const bgTy = Math.round(firstWord.y + firstWord.transform.translateY);
3108
3111
  context.translate(bgTx, bgTy);
3109
- if (wordOp.transform.scale !== 1) {
3110
- const halfWidth = wordOp.width / 2;
3112
+ if (firstWord.transform.scale !== 1) {
3113
+ const halfWidth = mergedWidth / 2;
3111
3114
  context.translate(halfWidth, 0);
3112
- context.scale(wordOp.transform.scale, wordOp.transform.scale);
3115
+ context.scale(firstWord.transform.scale, firstWord.transform.scale);
3113
3116
  context.translate(-halfWidth, 0);
3114
3117
  }
3115
- context.globalAlpha = wordOp.transform.opacity;
3116
- context.font = `${wordOp.fontWeight} ${wordOp.fontSize}px "${wordOp.fontFamily}"`;
3117
- context.textBaseline = "alphabetic";
3118
- if (wordOp.letterSpacing) {
3119
- context.letterSpacing = `${wordOp.letterSpacing}px`;
3120
- }
3121
- const bgTextWidth = wordOp.width;
3122
- const bgAscent = wordOp.fontSize * ASCENT_RATIO;
3123
- const bgDescent = wordOp.fontSize * DESCENT_RATIO;
3118
+ context.globalAlpha = firstWord.transform.opacity;
3119
+ const bgAscent = firstWord.fontSize * ASCENT_RATIO;
3120
+ const bgDescent = firstWord.fontSize * DESCENT_RATIO;
3124
3121
  const bgTextHeight = bgAscent + bgDescent;
3125
- const bgX = -wordOp.background.padding;
3126
- const bgY = -bgAscent - wordOp.background.padding;
3127
- const bgW = bgTextWidth + wordOp.background.padding * 2;
3128
- const bgH = bgTextHeight + wordOp.background.padding * 2;
3129
- const bgC = parseHex6(wordOp.background.color, wordOp.background.opacity);
3122
+ const bgX = -firstBg.padding;
3123
+ const bgY = -bgAscent - firstBg.padding;
3124
+ const bgW = mergedWidth + firstBg.padding * 2;
3125
+ const bgH = bgTextHeight + firstBg.padding * 2;
3126
+ const bgC = parseHex6(firstBg.color, firstBg.opacity);
3130
3127
  context.fillStyle = `rgba(${bgC.r},${bgC.g},${bgC.b},${bgC.a})`;
3131
3128
  context.beginPath();
3132
- roundRectPath(context, bgX, bgY, bgW, bgH, wordOp.background.borderRadius);
3129
+ roundRectPath(context, bgX, bgY, bgW, bgH, firstBg.borderRadius);
3133
3130
  context.fill();
3134
3131
  context.restore();
3132
+ bgIdx = nextIdx;
3135
3133
  }
3136
3134
  for (const wordOp of captionWordOps) {
3137
3135
  const displayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
@@ -4914,16 +4912,19 @@ var CaptionLayoutEngine = class {
4914
4912
  };
4915
4913
  }).filter((g) => g !== null);
4916
4914
  });
4915
+ const contentHeight = config.frameHeight - config.padding.top - config.padding.bottom;
4916
+ const contentWidth = config.frameWidth - config.padding.left - config.padding.right;
4917
+ const ASCENT_RATIO2 = 0.8;
4917
4918
  const calculateGroupY = (group) => {
4918
4919
  const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
4919
4920
  switch (config.verticalAlign) {
4920
4921
  case "top":
4921
- return config.fontSize * 1.5;
4922
+ return config.padding.top + config.fontSize * ASCENT_RATIO2;
4922
4923
  case "bottom":
4923
- return config.frameHeight - totalHeight - config.fontSize * 0.5;
4924
+ return config.frameHeight - config.padding.bottom - totalHeight + config.fontSize * ASCENT_RATIO2;
4924
4925
  case "middle":
4925
4926
  default:
4926
- return (config.frameHeight - totalHeight) / 2 + config.fontSize;
4927
+ return config.padding.top + (contentHeight - totalHeight) / 2 + config.fontSize * ASCENT_RATIO2;
4927
4928
  }
4928
4929
  };
4929
4930
  const allWordTexts = store.words.slice(0, store.length);
@@ -4931,12 +4932,12 @@ var CaptionLayoutEngine = class {
4931
4932
  const calculateLineX = (lineWidth) => {
4932
4933
  switch (config.horizontalAlign) {
4933
4934
  case "left":
4934
- return config.paddingLeft;
4935
+ return config.padding.left;
4935
4936
  case "right":
4936
- return config.frameWidth - lineWidth - config.paddingLeft;
4937
+ return config.frameWidth - lineWidth - config.padding.right;
4937
4938
  case "center":
4938
4939
  default:
4939
- return (config.frameWidth - lineWidth) / 2;
4940
+ return config.padding.left + (contentWidth - lineWidth) / 2;
4940
4941
  }
4941
4942
  };
4942
4943
  for (const group of groups) {
@@ -5809,7 +5810,7 @@ var RichCaptionRenderer = class {
5809
5810
  maxLines: computedMaxLines,
5810
5811
  verticalAlign,
5811
5812
  horizontalAlign,
5812
- paddingLeft: padding.left,
5813
+ padding,
5813
5814
  fontSize,
5814
5815
  fontFamily: font?.family ?? "Roboto",
5815
5816
  fontWeight: String(font?.weight ?? "400"),
@@ -609,6 +609,12 @@ interface WordTiming {
609
609
  end: number;
610
610
  confidence?: number;
611
611
  }
612
+ interface CaptionPadding {
613
+ top: number;
614
+ right: number;
615
+ bottom: number;
616
+ left: number;
617
+ }
612
618
  interface CaptionLayoutConfig {
613
619
  frameWidth: number;
614
620
  frameHeight: number;
@@ -616,7 +622,7 @@ interface CaptionLayoutConfig {
616
622
  maxLines: number;
617
623
  verticalAlign: "top" | "middle" | "bottom";
618
624
  horizontalAlign: "left" | "center" | "right";
619
- paddingLeft: number;
625
+ padding: CaptionPadding;
620
626
  fontSize: number;
621
627
  fontFamily: string;
622
628
  fontWeight: string | number;
@@ -1285,4 +1291,4 @@ declare function createTextEngine(opts?: {
1285
1291
  destroy(): void;
1286
1292
  }>;
1287
1293
 
1288
- export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, MediaRecorderFallback, type MoveToCommand, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, WebCodecsEncoder, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createMediaRecorderFallback, createTextEngine, createVideoEncoder, createWebCodecsEncoder, createWebPainter, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, initResvg, isDrawCaptionWordOp, isMediaRecorderSupported, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };
1294
+ export { ASCENT_RATIO, type AnimationDirection, type AnimationStyle, type ArcCommand, type BackgroundConfig, type BoundingBox, type CanvasRichCaptionAsset, CanvasRichCaptionAssetSchema, type CanvasRichTextAsset, CanvasRichTextAssetSchema, type CanvasSvgAsset, CanvasSvgAssetSchema, type CaptionGroup, type CaptionLayout, type CaptionLayoutConfig, CaptionLayoutEngine, type CaptionLine, type CaptionPadding, type ClosePathCommand, type CubicBezierCommand, DESCENT_RATIO, type DrawOp, type EngineInit, type FastVideoOptions, type FastVideoResult, type FontConfig, FontRegistry, type FrameSchedule, type Glyph, type GradientSpec, type IVideoEncoder, type LineToCommand, MediaRecorderFallback, type MoveToCommand, type NormalizedPathCommand, type ParagraphDirection, type ParsedPathCommand, type PathCommandType, type Point2D, type PositionedWord, type QuadraticBezierCommand, type RGBA, type RenderFrame, type RenderStats, type Renderer, type ResvgRenderOptions, type ResvgRenderResult, type RichCaptionGeneratorConfig, type RichCaptionRendererOptions, type ShadowConfig, type ShapedLine, type ShapedWord, type ShapedWordGlyph, type ShotstackRichTextAsset, type ShotstackSvgAsset, type StrokeConfig, type StrokeSpec, type ValidAsset, type VideoEncoderCapabilities, type VideoEncoderConfig, type VideoEncoderProgress, WORD_BG_BORDER_RADIUS, WORD_BG_OPACITY, WORD_BG_PADDING_RATIO, WebCodecsEncoder, type WordAnimationConfig, type WordAnimationState, type WordTiming, WordTimingStore, arcToCubicBeziers, breakIntoLines, calculateAnimationStatesForGroup, commandsToPathString, computeSimplePathBounds, containsRTLCharacters, createDefaultGeneratorConfig, createFrameSchedule, createMediaRecorderFallback, createTextEngine, createVideoEncoder, createWebCodecsEncoder, createWebPainter, detectParagraphDirection, detectParagraphDirectionFromWords, detectPlatform, detectSubtitleFormat, extractCaptionPadding, findWordAtTime, generateRichCaptionDrawOps, generateRichCaptionFrame, generateShapePathData, getDefaultAnimationConfig, getDrawCaptionWordOps, getEncoderCapabilities, getEncoderWarning, getVisibleText, groupWordsByPause, initResvg, isDrawCaptionWordOp, isMediaRecorderSupported, isRTLText, isWebCodecsH264Supported, mirrorAnimationDirection, normalizePath, normalizePathString, parseSubtitleToWords, parseSvgPath, quadraticToCubic, renderSvgAssetToPng, renderSvgToPng, reorderWordsForLine, richCaptionAssetSchema, shapeToSvgString };
package/dist/entry.web.js CHANGED
@@ -34474,10 +34474,11 @@ function calculateTypewriterState(ctx, charCount, speed) {
34474
34474
  isActive: isWordActive(ctx)
34475
34475
  };
34476
34476
  }
34477
- function calculateNoneState(ctx) {
34477
+ function calculateNoneState(_ctx) {
34478
34478
  return {
34479
34479
  opacity: 1,
34480
- isActive: isWordActive(ctx)
34480
+ isActive: false,
34481
+ fillProgress: 0
34481
34482
  };
34482
34483
  }
34483
34484
  function calculateWordAnimationState(wordStart, wordEnd, currentTime, config2, activeScale = 1, charCount = 0, fontSize = 48, isRTL = false) {
@@ -34520,7 +34521,7 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config2, a
34520
34521
  break;
34521
34522
  }
34522
34523
  const mergedState = { ...baseState, ...partialState };
34523
- if (mergedState.isActive && activeScale !== 1 && config2.style !== "pop") {
34524
+ if (mergedState.isActive && activeScale !== 1 && config2.style !== "pop" && config2.style !== "none") {
34524
34525
  mergedState.scale = activeScale;
34525
34526
  }
34526
34527
  return mergedState;
@@ -34582,7 +34583,10 @@ function extractStrokeConfig(asset, isActive) {
34582
34583
  if (!baseStroke && !activeStroke) {
34583
34584
  return void 0;
34584
34585
  }
34585
- if (isActive && activeStroke) {
34586
+ if (isActive) {
34587
+ if (!activeStroke) {
34588
+ return void 0;
34589
+ }
34586
34590
  return {
34587
34591
  width: activeStroke.width ?? baseStroke?.width ?? 0,
34588
34592
  color: activeStroke.color ?? baseStroke?.color ?? "#000000",
@@ -34598,7 +34602,10 @@ function extractStrokeConfig(asset, isActive) {
34598
34602
  }
34599
34603
  return void 0;
34600
34604
  }
34601
- function extractShadowConfig(asset) {
34605
+ function extractShadowConfig(asset, isActive) {
34606
+ if (isActive) {
34607
+ return void 0;
34608
+ }
34602
34609
  const shadow = asset.shadow;
34603
34610
  if (!shadow) {
34604
34611
  return void 0;
@@ -34705,27 +34712,10 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
34705
34712
  visibleCharacters: animState.visibleCharacters,
34706
34713
  letterSpacing: fontConfig.letterSpacing > 0 ? fontConfig.letterSpacing : void 0,
34707
34714
  stroke: extractStrokeConfig(asset, isActive),
34708
- shadow: extractShadowConfig(asset),
34715
+ shadow: extractShadowConfig(asset, isActive),
34709
34716
  background: extractBackgroundConfig(asset, isActive, fontConfig.size)
34710
34717
  };
34711
34718
  }
34712
- function calculateGroupBounds(activeGroup, padding, frameWidth, activeScale, fontSize) {
34713
- let minY = Infinity;
34714
- let maxY = -Infinity;
34715
- for (const line of activeGroup.lines) {
34716
- const lineY = line.y - line.height * ASCENT_RATIO;
34717
- const lineBottom = line.y + line.height * DESCENT_RATIO;
34718
- if (lineY < minY) minY = lineY;
34719
- if (lineBottom > maxY) maxY = lineBottom;
34720
- }
34721
- const scaleExpansion = activeScale > 1 ? (activeScale - 1) * fontSize : 0;
34722
- return {
34723
- bgX: 0,
34724
- bgY: minY - padding.top - scaleExpansion,
34725
- bgWidth: frameWidth,
34726
- bgHeight: maxY - minY + padding.top + padding.bottom + scaleExpansion * 2
34727
- };
34728
- }
34729
34719
  function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, config2) {
34730
34720
  if (layout.store.length === 0) {
34731
34721
  return [];
@@ -34745,48 +34735,35 @@ function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, co
34745
34735
  fontConfig.size
34746
34736
  );
34747
34737
  const ops = [];
34748
- const activeGroup = layout.groups.find(
34749
- (g) => frameTimeMs >= g.startTime && frameTimeMs <= g.endTime
34750
- );
34751
- if (activeGroup && activeGroup.lines.length > 0) {
34752
- const padding = extractCaptionPadding(asset);
34753
- const { bgX, bgY, bgWidth, bgHeight } = calculateGroupBounds(
34754
- activeGroup,
34755
- padding,
34756
- config2.frameWidth,
34757
- activeScale,
34758
- fontConfig.size
34759
- );
34760
- const captionBg = extractCaptionBackground(asset);
34761
- if (captionBg) {
34762
- ops.push({
34763
- op: "DrawCaptionBackground",
34764
- x: bgX,
34765
- y: bgY,
34766
- width: bgWidth,
34767
- height: bgHeight,
34768
- color: captionBg.color,
34769
- opacity: captionBg.opacity,
34770
- borderRadius: captionBg.borderRadius
34771
- });
34772
- }
34773
- const borderConfig = extractCaptionBorder(asset);
34774
- if (borderConfig) {
34775
- const halfBorder = borderConfig.width / 2;
34776
- ops.push({
34777
- op: "RectangleStroke",
34778
- x: bgX + halfBorder,
34779
- y: bgY + halfBorder,
34780
- width: bgWidth - borderConfig.width,
34781
- height: bgHeight - borderConfig.width,
34782
- stroke: {
34783
- width: borderConfig.width,
34784
- color: borderConfig.color,
34785
- opacity: borderConfig.opacity
34786
- },
34787
- borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
34788
- });
34789
- }
34738
+ const captionBg = extractCaptionBackground(asset);
34739
+ if (captionBg) {
34740
+ ops.push({
34741
+ op: "DrawCaptionBackground",
34742
+ x: 0,
34743
+ y: 0,
34744
+ width: config2.frameWidth,
34745
+ height: config2.frameHeight,
34746
+ color: captionBg.color,
34747
+ opacity: captionBg.opacity,
34748
+ borderRadius: captionBg.borderRadius
34749
+ });
34750
+ }
34751
+ const borderConfig = extractCaptionBorder(asset);
34752
+ if (borderConfig) {
34753
+ const halfBorder = borderConfig.width / 2;
34754
+ ops.push({
34755
+ op: "RectangleStroke",
34756
+ x: halfBorder,
34757
+ y: halfBorder,
34758
+ width: config2.frameWidth - borderConfig.width,
34759
+ height: config2.frameHeight - borderConfig.width,
34760
+ stroke: {
34761
+ width: borderConfig.width,
34762
+ color: borderConfig.color,
34763
+ opacity: borderConfig.opacity
34764
+ },
34765
+ borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
34766
+ });
34790
34767
  }
34791
34768
  const nonActiveOps = [];
34792
34769
  const activeOps = [];
@@ -35066,38 +35043,59 @@ function createWebPainter(canvas) {
35066
35043
  captionWordOps.push(nextOp);
35067
35044
  i++;
35068
35045
  }
35069
- for (const wordOp of captionWordOps) {
35070
- if (!wordOp.background) continue;
35046
+ let bgIdx = 0;
35047
+ while (bgIdx < captionWordOps.length) {
35048
+ const wordOp = captionWordOps[bgIdx];
35049
+ if (!wordOp.background) {
35050
+ bgIdx++;
35051
+ continue;
35052
+ }
35071
35053
  const wordDisplayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
35072
- if (wordDisplayText.length === 0) continue;
35054
+ if (wordDisplayText.length === 0) {
35055
+ bgIdx++;
35056
+ continue;
35057
+ }
35058
+ const mergeGroup = [wordOp];
35059
+ let nextIdx = bgIdx + 1;
35060
+ while (nextIdx < captionWordOps.length) {
35061
+ const nextWord = captionWordOps[nextIdx];
35062
+ if (!nextWord.background) break;
35063
+ const nextDisplay = getVisibleText(nextWord.text, nextWord.visibleCharacters, nextWord.isRTL);
35064
+ if (nextDisplay.length === 0) break;
35065
+ if (Math.round(nextWord.y) !== Math.round(wordOp.y)) break;
35066
+ if (nextWord.background.color !== wordOp.background.color) break;
35067
+ mergeGroup.push(nextWord);
35068
+ nextIdx++;
35069
+ }
35070
+ const firstWord = mergeGroup[0];
35071
+ const lastWord = mergeGroup[mergeGroup.length - 1];
35072
+ const firstBg = firstWord.background;
35073
+ const mergedLeft = firstWord.x + firstWord.transform.translateX;
35074
+ const mergedRight = lastWord.x + lastWord.transform.translateX + lastWord.width;
35075
+ const mergedWidth = mergedRight - mergedLeft;
35073
35076
  ctx.save();
35074
- const bgTx = Math.round(wordOp.x + wordOp.transform.translateX);
35075
- const bgTy = Math.round(wordOp.y + wordOp.transform.translateY);
35077
+ const bgTx = Math.round(mergedLeft);
35078
+ const bgTy = Math.round(firstWord.y + firstWord.transform.translateY);
35076
35079
  ctx.translate(bgTx, bgTy);
35077
- if (wordOp.transform.scale !== 1) {
35078
- const halfWidth = wordOp.width / 2;
35080
+ if (firstWord.transform.scale !== 1) {
35081
+ const halfWidth = mergedWidth / 2;
35079
35082
  ctx.translate(halfWidth, 0);
35080
- ctx.scale(wordOp.transform.scale, wordOp.transform.scale);
35083
+ ctx.scale(firstWord.transform.scale, firstWord.transform.scale);
35081
35084
  ctx.translate(-halfWidth, 0);
35082
35085
  }
35083
- ctx.globalAlpha = wordOp.transform.opacity;
35084
- ctx.font = `${wordOp.fontWeight} ${wordOp.fontSize}px "${wordOp.fontFamily}"`;
35085
- ctx.textBaseline = "alphabetic";
35086
- if (wordOp.letterSpacing) {
35087
- ctx.letterSpacing = `${wordOp.letterSpacing}px`;
35088
- }
35089
- const bgTextWidth = wordOp.width;
35090
- const bgAscent = wordOp.fontSize * ASCENT_RATIO;
35091
- const bgDescent = wordOp.fontSize * DESCENT_RATIO;
35086
+ ctx.globalAlpha = firstWord.transform.opacity;
35087
+ const bgAscent = firstWord.fontSize * ASCENT_RATIO;
35088
+ const bgDescent = firstWord.fontSize * DESCENT_RATIO;
35092
35089
  const bgTextHeight = bgAscent + bgDescent;
35093
- const bgX = -wordOp.background.padding;
35094
- const bgY = -bgAscent - wordOp.background.padding;
35095
- const bgW = bgTextWidth + wordOp.background.padding * 2;
35096
- const bgH = bgTextHeight + wordOp.background.padding * 2;
35097
- const bgC = parseHex6(wordOp.background.color, wordOp.background.opacity);
35090
+ const bgX = -firstBg.padding;
35091
+ const bgY = -bgAscent - firstBg.padding;
35092
+ const bgW = mergedWidth + firstBg.padding * 2;
35093
+ const bgH = bgTextHeight + firstBg.padding * 2;
35094
+ const bgC = parseHex6(firstBg.color, firstBg.opacity);
35098
35095
  ctx.fillStyle = `rgba(${bgC.r},${bgC.g},${bgC.b},${bgC.a})`;
35099
- drawRoundedRect(ctx, bgX, bgY, bgW, bgH, wordOp.background.borderRadius);
35096
+ drawRoundedRect(ctx, bgX, bgY, bgW, bgH, firstBg.borderRadius);
35100
35097
  ctx.restore();
35098
+ bgIdx = nextIdx;
35101
35099
  }
35102
35100
  for (const wordOp of captionWordOps) {
35103
35101
  const displayText = getVisibleText(wordOp.text, wordOp.visibleCharacters, wordOp.isRTL);
@@ -37220,16 +37218,19 @@ var CaptionLayoutEngine = class {
37220
37218
  };
37221
37219
  }).filter((g) => g !== null);
37222
37220
  });
37221
+ const contentHeight = config2.frameHeight - config2.padding.top - config2.padding.bottom;
37222
+ const contentWidth = config2.frameWidth - config2.padding.left - config2.padding.right;
37223
+ const ASCENT_RATIO2 = 0.8;
37223
37224
  const calculateGroupY = (group) => {
37224
37225
  const totalHeight = group.lines.length * config2.fontSize * config2.lineHeight;
37225
37226
  switch (config2.verticalAlign) {
37226
37227
  case "top":
37227
- return config2.fontSize * 1.5;
37228
+ return config2.padding.top + config2.fontSize * ASCENT_RATIO2;
37228
37229
  case "bottom":
37229
- return config2.frameHeight - totalHeight - config2.fontSize * 0.5;
37230
+ return config2.frameHeight - config2.padding.bottom - totalHeight + config2.fontSize * ASCENT_RATIO2;
37230
37231
  case "middle":
37231
37232
  default:
37232
- return (config2.frameHeight - totalHeight) / 2 + config2.fontSize;
37233
+ return config2.padding.top + (contentHeight - totalHeight) / 2 + config2.fontSize * ASCENT_RATIO2;
37233
37234
  }
37234
37235
  };
37235
37236
  const allWordTexts = store.words.slice(0, store.length);
@@ -37237,12 +37238,12 @@ var CaptionLayoutEngine = class {
37237
37238
  const calculateLineX = (lineWidth) => {
37238
37239
  switch (config2.horizontalAlign) {
37239
37240
  case "left":
37240
- return config2.paddingLeft;
37241
+ return config2.padding.left;
37241
37242
  case "right":
37242
- return config2.frameWidth - lineWidth - config2.paddingLeft;
37243
+ return config2.frameWidth - lineWidth - config2.padding.right;
37243
37244
  case "center":
37244
37245
  default:
37245
- return (config2.frameWidth - lineWidth) / 2;
37246
+ return config2.padding.left + (contentWidth - lineWidth) / 2;
37246
37247
  }
37247
37248
  };
37248
37249
  for (const group of groups) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotstack/shotstack-canvas",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
5
  "type": "module",
6
6
  "main": "./dist/entry.node.cjs",
@@ -32,8 +32,13 @@
32
32
  "example:video": "node examples/node-video.mjs",
33
33
  "example:web": "vite dev examples/web-example",
34
34
  "test:caption-web": "vite dev examples/caption-tests",
35
+ "test:padding-node": "node examples/captionpaddingtests/node-test.mjs",
36
+ "test:padding-web": "vite dev examples/captionpaddingtests",
37
+ "test:fixes-node": "node examples/caption-fix-tests/node-test.mjs",
38
+ "test:fixes-web": "vite dev examples/caption-fix-tests",
35
39
  "prepublishOnly": "node scripts/publish-guard.cjs && pnpm build",
36
- "test": "node --test tests/build-verify.mjs"
40
+ "test": "node --test tests/build-verify.mjs",
41
+ "test:logic": "node --test tests/caption-logic.mjs"
37
42
  },
38
43
  "publishConfig": {
39
44
  "access": "public",