@shotstack/shotstack-canvas 2.0.9 → 2.0.10

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.
@@ -19,8 +19,7 @@ import {
19
19
  svgTransformSchema,
20
20
  svgGradientStopSchema,
21
21
  richCaptionActiveSchema as baseCaptionActiveSchema,
22
- richCaptionWordAnimationSchema as baseCaptionWordAnimationSchema,
23
- wordTimingSchema as baseWordTimingSchema
22
+ richCaptionWordAnimationSchema as baseCaptionWordAnimationSchema
24
23
  } from "@shotstack/schemas/zod";
25
24
 
26
25
  // src/config/canvas-constants.ts
@@ -177,7 +176,7 @@ var CanvasRichTextAssetSchema = richTextAssetSchema.extend({
177
176
  customFonts: z.array(customFontSchema).optional()
178
177
  }).strict();
179
178
  var CanvasSvgAssetSchema = svgAssetSchema;
180
- var wordTimingSchema = baseWordTimingSchema.extend({
179
+ var wordTimingSchema = z.object({
181
180
  text: z.string().min(1),
182
181
  start: z.number().min(0),
183
182
  end: z.number().min(0),
@@ -218,27 +217,18 @@ var richCaptionAssetSchema = z.object({
218
217
  stroke: canvasStrokeSchema.optional(),
219
218
  shadow: canvasShadowSchema.optional(),
220
219
  background: canvasBackgroundSchema.optional(),
220
+ border: borderSchema.optional(),
221
221
  padding: paddingSchema.optional(),
222
222
  align: canvasAlignmentSchema.optional(),
223
223
  active: richCaptionActiveSchema.optional(),
224
224
  wordAnimation: richCaptionWordAnimationSchema.optional(),
225
- position: z.enum(["top", "center", "bottom"]).default("bottom"),
226
- maxWidth: z.number().min(0.1).max(1).default(0.9),
227
- maxLines: z.number().int().min(1).max(10).default(2),
228
225
  customFonts: z.array(customFontSchema).optional()
229
226
  }).superRefine((data, ctx) => {
230
- if (data.src && data.words) {
231
- ctx.addIssue({
232
- code: z.ZodIssueCode.custom,
233
- message: "src and words are mutually exclusive",
234
- path: ["src"]
235
- });
236
- }
237
227
  if (!data.src && !data.words) {
238
228
  ctx.addIssue({
239
229
  code: z.ZodIssueCode.custom,
240
230
  message: "Either src or words must be provided",
241
- path: ["words"]
231
+ path: ["src"]
242
232
  });
243
233
  }
244
234
  });
@@ -2104,6 +2094,12 @@ async function createNodePainter(opts) {
2104
2094
  if (!fill) {
2105
2095
  fill = makeGradientFromBBox(context, fillOp.fill, localBBox);
2106
2096
  gradientCache.set(cacheKey, fill);
2097
+ if (gradientCache.size > GRADIENT_CACHE_MAX) {
2098
+ const firstEntry = gradientCache.keys().next();
2099
+ if (!firstEntry.done) {
2100
+ gradientCache.delete(firstEntry.value);
2101
+ }
2102
+ }
2107
2103
  }
2108
2104
  context.fillStyle = fill;
2109
2105
  context.beginPath();
@@ -2451,7 +2447,7 @@ async function createNodePainter(opts) {
2451
2447
  toRawRGBA() {
2452
2448
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
2453
2449
  return {
2454
- data: new Uint8ClampedArray(imageData.data),
2450
+ data: imageData.data,
2455
2451
  width: canvas.width,
2456
2452
  height: canvas.height
2457
2453
  };
@@ -4074,7 +4070,7 @@ var CaptionLayoutEngine = class {
4074
4070
  }
4075
4071
  }
4076
4072
  const wordGroups = groupWordsByPause(store, config.pauseThreshold);
4077
- const pixelMaxWidth = config.frameWidth * config.maxWidth;
4073
+ const pixelMaxWidth = config.availableWidth;
4078
4074
  let spaceWidth;
4079
4075
  if (config.measureTextWidth) {
4080
4076
  const fontString = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
@@ -4104,31 +4100,45 @@ var CaptionLayoutEngine = class {
4104
4100
  };
4105
4101
  });
4106
4102
  const allWordIndices = lines.flatMap((l) => l.wordIndices);
4103
+ if (allWordIndices.length === 0) {
4104
+ return null;
4105
+ }
4107
4106
  return {
4108
4107
  wordIndices: allWordIndices,
4109
4108
  startTime: store.startTimes[allWordIndices[0]],
4110
4109
  endTime: store.endTimes[allWordIndices[allWordIndices.length - 1]],
4111
4110
  lines
4112
4111
  };
4113
- });
4112
+ }).filter((g) => g !== null);
4114
4113
  });
4115
4114
  const calculateGroupY = (group) => {
4116
4115
  const totalHeight = group.lines.length * config.fontSize * config.lineHeight;
4117
- switch (config.position) {
4116
+ switch (config.verticalAlign) {
4118
4117
  case "top":
4119
4118
  return config.fontSize * 1.5;
4120
4119
  case "bottom":
4121
4120
  return config.frameHeight - totalHeight - config.fontSize * 0.5;
4122
- case "center":
4121
+ case "middle":
4123
4122
  default:
4124
4123
  return (config.frameHeight - totalHeight) / 2 + config.fontSize;
4125
4124
  }
4126
4125
  };
4126
+ const calculateLineX = (lineWidth) => {
4127
+ switch (config.horizontalAlign) {
4128
+ case "left":
4129
+ return config.paddingLeft;
4130
+ case "right":
4131
+ return config.frameWidth - lineWidth - config.paddingLeft;
4132
+ case "center":
4133
+ default:
4134
+ return (config.frameWidth - lineWidth) / 2;
4135
+ }
4136
+ };
4127
4137
  for (const group of groups) {
4128
4138
  const baseY = calculateGroupY(group);
4129
4139
  for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
4130
4140
  const line = group.lines[lineIdx];
4131
- line.x = (config.frameWidth - line.width) / 2;
4141
+ line.x = calculateLineX(line.width);
4132
4142
  line.y = baseY + lineIdx * config.fontSize * config.lineHeight;
4133
4143
  let xCursor = line.x;
4134
4144
  for (const wordIdx of line.wordIndices) {
@@ -4419,6 +4429,7 @@ function calculateNoneState(ctx) {
4419
4429
  };
4420
4430
  }
4421
4431
  function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, activeScale = 1, charCount = 0, fontSize = 48) {
4432
+ const safeSpeed = config.speed > 0 ? config.speed : 1;
4422
4433
  const ctx = {
4423
4434
  wordStart,
4424
4435
  wordEnd,
@@ -4429,25 +4440,25 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config, ac
4429
4440
  let partialState;
4430
4441
  switch (config.style) {
4431
4442
  case "karaoke":
4432
- partialState = calculateKaraokeState(ctx, config.speed);
4443
+ partialState = calculateKaraokeState(ctx, safeSpeed);
4433
4444
  break;
4434
4445
  case "highlight":
4435
4446
  partialState = calculateHighlightState(ctx);
4436
4447
  break;
4437
4448
  case "pop":
4438
- partialState = calculatePopState(ctx, activeScale, config.speed);
4449
+ partialState = calculatePopState(ctx, activeScale, safeSpeed);
4439
4450
  break;
4440
4451
  case "fade":
4441
- partialState = calculateFadeState(ctx, config.speed);
4452
+ partialState = calculateFadeState(ctx, safeSpeed);
4442
4453
  break;
4443
4454
  case "slide":
4444
- partialState = calculateSlideState(ctx, config.direction, config.speed, fontSize);
4455
+ partialState = calculateSlideState(ctx, config.direction, safeSpeed, fontSize);
4445
4456
  break;
4446
4457
  case "bounce":
4447
- partialState = calculateBounceState(ctx, config.speed, fontSize);
4458
+ partialState = calculateBounceState(ctx, safeSpeed, fontSize);
4448
4459
  break;
4449
4460
  case "typewriter":
4450
- partialState = calculateTypewriterState(ctx, charCount, config.speed);
4461
+ partialState = calculateTypewriterState(ctx, charCount, safeSpeed);
4451
4462
  break;
4452
4463
  case "none":
4453
4464
  default:
@@ -4571,6 +4582,22 @@ function extractCaptionBackground(asset) {
4571
4582
  opacity: bg.opacity ?? 1
4572
4583
  };
4573
4584
  }
4585
+ function extractCaptionBackgroundBorderRadius(asset) {
4586
+ const bg = asset.background;
4587
+ return bg?.borderRadius ?? 0;
4588
+ }
4589
+ function extractCaptionBorder(asset) {
4590
+ const border = asset.border;
4591
+ if (!border || !border.width || border.width <= 0) {
4592
+ return void 0;
4593
+ }
4594
+ return {
4595
+ width: border.width,
4596
+ color: border.color ?? "#000000",
4597
+ opacity: border.opacity ?? 1,
4598
+ radius: border.radius ?? 0
4599
+ };
4600
+ }
4574
4601
  function extractAnimationConfig(asset) {
4575
4602
  const wordAnim = asset.wordAnimation;
4576
4603
  if (!wordAnim) {
@@ -4616,6 +4643,28 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
4616
4643
  background: extractBackgroundConfig(asset, isActive)
4617
4644
  };
4618
4645
  }
4646
+ function calculateGroupBounds(activeGroup, padding) {
4647
+ let minX = Infinity;
4648
+ let maxX = -Infinity;
4649
+ let minY = Infinity;
4650
+ let maxY = -Infinity;
4651
+ for (const line of activeGroup.lines) {
4652
+ const lineX = line.x;
4653
+ const lineRight = line.x + line.width;
4654
+ const lineY = line.y - line.height * 0.8;
4655
+ const lineBottom = line.y + line.height * 0.2;
4656
+ if (lineX < minX) minX = lineX;
4657
+ if (lineRight > maxX) maxX = lineRight;
4658
+ if (lineY < minY) minY = lineY;
4659
+ if (lineBottom > maxY) maxY = lineBottom;
4660
+ }
4661
+ return {
4662
+ bgX: minX - padding.left,
4663
+ bgY: minY - padding.top,
4664
+ bgWidth: maxX - minX + padding.left + padding.right,
4665
+ bgHeight: maxY - minY + padding.top + padding.bottom
4666
+ };
4667
+ }
4619
4668
  function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, _config) {
4620
4669
  if (layout.store.length === 0) {
4621
4670
  return [];
@@ -4635,36 +4684,40 @@ function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, _c
4635
4684
  fontConfig.size
4636
4685
  );
4637
4686
  const ops = [];
4638
- const captionBg = extractCaptionBackground(asset);
4639
- if (captionBg) {
4640
- const activeGroup = layout.groups.find(
4641
- (g) => frameTimeMs >= g.startTime && frameTimeMs <= g.endTime
4642
- );
4643
- if (activeGroup && activeGroup.lines.length > 0) {
4644
- const padding = extractCaptionPadding(asset);
4645
- let minX = Infinity;
4646
- let maxX = -Infinity;
4647
- let minY = Infinity;
4648
- let maxY = -Infinity;
4649
- for (const line of activeGroup.lines) {
4650
- const lineX = line.x;
4651
- const lineRight = line.x + line.width;
4652
- const lineY = line.y - line.height * 0.8;
4653
- const lineBottom = line.y + line.height * 0.2;
4654
- if (lineX < minX) minX = lineX;
4655
- if (lineRight > maxX) maxX = lineRight;
4656
- if (lineY < minY) minY = lineY;
4657
- if (lineBottom > maxY) maxY = lineBottom;
4658
- }
4687
+ const activeGroup = layout.groups.find(
4688
+ (g) => frameTimeMs >= g.startTime && frameTimeMs <= g.endTime
4689
+ );
4690
+ if (activeGroup && activeGroup.lines.length > 0) {
4691
+ const padding = extractCaptionPadding(asset);
4692
+ const { bgX, bgY, bgWidth, bgHeight } = calculateGroupBounds(activeGroup, padding);
4693
+ const captionBg = extractCaptionBackground(asset);
4694
+ if (captionBg) {
4659
4695
  ops.push({
4660
4696
  op: "DrawCaptionBackground",
4661
- x: minX - padding.left,
4662
- y: minY - padding.top,
4663
- width: maxX - minX + padding.left + padding.right,
4664
- height: maxY - minY + padding.top + padding.bottom,
4697
+ x: bgX,
4698
+ y: bgY,
4699
+ width: bgWidth,
4700
+ height: bgHeight,
4665
4701
  color: captionBg.color,
4666
4702
  opacity: captionBg.opacity,
4667
- borderRadius: 8
4703
+ borderRadius: extractCaptionBackgroundBorderRadius(asset)
4704
+ });
4705
+ }
4706
+ const borderConfig = extractCaptionBorder(asset);
4707
+ if (borderConfig) {
4708
+ const halfBorder = borderConfig.width / 2;
4709
+ ops.push({
4710
+ op: "RectangleStroke",
4711
+ x: bgX + halfBorder,
4712
+ y: bgY + halfBorder,
4713
+ width: bgWidth - borderConfig.width,
4714
+ height: bgHeight - borderConfig.width,
4715
+ stroke: {
4716
+ width: borderConfig.width,
4717
+ color: borderConfig.color,
4718
+ opacity: borderConfig.opacity
4719
+ },
4720
+ borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
4668
4721
  });
4669
4722
  }
4670
4723
  }
@@ -5488,18 +5541,28 @@ var RichCaptionRenderer = class {
5488
5541
  const font = asset.font;
5489
5542
  const style = asset.style;
5490
5543
  const measureTextWidth = await createCanvasTextMeasurer();
5544
+ const fontSize = font?.size ?? 24;
5545
+ const lineHeight = style?.lineHeight ?? 1.2;
5546
+ const padding = this.extractPadding(asset);
5547
+ const availableWidth = this.width - padding.left - padding.right;
5548
+ const availableHeight = this.height - padding.top - padding.bottom;
5549
+ const computedMaxLines = Math.max(1, Math.min(10, Math.floor(availableHeight / (fontSize * lineHeight))));
5550
+ const verticalAlign = this.mapVerticalAlign(asset);
5551
+ const horizontalAlign = this.mapHorizontalAlign(asset);
5491
5552
  const layoutConfig = {
5492
5553
  frameWidth: this.width,
5493
5554
  frameHeight: this.height,
5494
- maxWidth: asset.maxWidth ?? 0.9,
5495
- maxLines: asset.maxLines ?? 2,
5496
- position: asset.position ?? "bottom",
5497
- fontSize: font?.size ?? 24,
5555
+ availableWidth,
5556
+ maxLines: computedMaxLines,
5557
+ verticalAlign,
5558
+ horizontalAlign,
5559
+ paddingLeft: padding.left,
5560
+ fontSize,
5498
5561
  fontFamily: font?.family ?? "Roboto",
5499
5562
  fontWeight: String(font?.weight ?? "400"),
5500
5563
  letterSpacing: style?.letterSpacing ?? 0,
5501
5564
  wordSpacing: typeof style?.wordSpacing === "number" ? style.wordSpacing : 0,
5502
- lineHeight: style?.lineHeight ?? 1.2,
5565
+ lineHeight,
5503
5566
  textTransform: style?.textTransform ?? "none",
5504
5567
  pauseThreshold: 500,
5505
5568
  measureTextWidth
@@ -5611,6 +5674,7 @@ var RichCaptionRenderer = class {
5611
5674
  const totalTimeMs = performance.now() - totalStart;
5612
5675
  const realtimeMultiplier = duration / (totalTimeMs / 1e3);
5613
5676
  this.logCompletion(totalTimeMs, realtimeMultiplier);
5677
+ encoder.close();
5614
5678
  return outputPath;
5615
5679
  } catch (error) {
5616
5680
  encoder.close();
@@ -5727,6 +5791,33 @@ var RichCaptionRenderer = class {
5727
5791
  clearCache() {
5728
5792
  this.layoutEngine?.clearCache();
5729
5793
  }
5794
+ extractPadding(asset) {
5795
+ const padding = asset.padding;
5796
+ if (!padding) {
5797
+ return { top: 0, right: 0, bottom: 0, left: 0 };
5798
+ }
5799
+ if (typeof padding === "number") {
5800
+ return { top: padding, right: padding, bottom: padding, left: padding };
5801
+ }
5802
+ return {
5803
+ top: padding.top ?? 0,
5804
+ right: padding.right ?? 0,
5805
+ bottom: padding.bottom ?? 0,
5806
+ left: padding.left ?? 0
5807
+ };
5808
+ }
5809
+ mapVerticalAlign(asset) {
5810
+ const vertical = asset.align?.vertical;
5811
+ if (vertical === "top") return "top";
5812
+ if (vertical === "middle") return "middle";
5813
+ return "bottom";
5814
+ }
5815
+ mapHorizontalAlign(asset) {
5816
+ const horizontal = asset.align?.horizontal;
5817
+ if (horizontal === "left") return "left";
5818
+ if (horizontal === "right") return "right";
5819
+ return "center";
5820
+ }
5730
5821
  extractAnimationStyle() {
5731
5822
  const wordAnim = this.currentAsset?.wordAnimation;
5732
5823
  return wordAnim?.style ?? "highlight";
@@ -263,7 +263,7 @@ declare const richCaptionAssetSchema: z.ZodObject<{
263
263
  start: z.ZodNumber;
264
264
  end: z.ZodNumber;
265
265
  confidence: z.ZodOptional<z.ZodNumber>;
266
- }, z.core.$strict>>>;
266
+ }, z.core.$strip>>>;
267
267
  font: z.ZodOptional<z.ZodObject<{
268
268
  family: z.ZodDefault<z.ZodString>;
269
269
  size: z.ZodDefault<z.ZodNumber>;
@@ -316,6 +316,12 @@ declare const richCaptionAssetSchema: z.ZodObject<{
316
316
  color: z.ZodOptional<z.ZodString>;
317
317
  opacity: z.ZodDefault<z.ZodNumber>;
318
318
  }, z.core.$strict>>;
319
+ border: z.ZodOptional<z.ZodObject<{
320
+ width: z.ZodDefault<z.ZodNumber>;
321
+ color: z.ZodDefault<z.ZodString>;
322
+ opacity: z.ZodDefault<z.ZodNumber>;
323
+ radius: z.ZodDefault<z.ZodNumber>;
324
+ }, z.core.$strip>>;
319
325
  padding: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{
320
326
  top: z.ZodDefault<z.ZodNumber>;
321
327
  right: z.ZodDefault<z.ZodNumber>;
@@ -366,13 +372,6 @@ declare const richCaptionAssetSchema: z.ZodObject<{
366
372
  down: "down";
367
373
  }>>;
368
374
  }, z.core.$strict>>;
369
- position: z.ZodDefault<z.ZodEnum<{
370
- center: "center";
371
- top: "top";
372
- bottom: "bottom";
373
- }>>;
374
- maxWidth: z.ZodDefault<z.ZodNumber>;
375
- maxLines: z.ZodDefault<z.ZodNumber>;
376
375
  customFonts: z.ZodOptional<z.ZodArray<z.ZodObject<{
377
376
  src: z.ZodString;
378
377
  family: z.ZodString;
@@ -389,7 +388,7 @@ declare const CanvasRichCaptionAssetSchema: z.ZodObject<{
389
388
  start: z.ZodNumber;
390
389
  end: z.ZodNumber;
391
390
  confidence: z.ZodOptional<z.ZodNumber>;
392
- }, z.core.$strict>>>;
391
+ }, z.core.$strip>>>;
393
392
  font: z.ZodOptional<z.ZodObject<{
394
393
  family: z.ZodDefault<z.ZodString>;
395
394
  size: z.ZodDefault<z.ZodNumber>;
@@ -442,6 +441,12 @@ declare const CanvasRichCaptionAssetSchema: z.ZodObject<{
442
441
  color: z.ZodOptional<z.ZodString>;
443
442
  opacity: z.ZodDefault<z.ZodNumber>;
444
443
  }, z.core.$strict>>;
444
+ border: z.ZodOptional<z.ZodObject<{
445
+ width: z.ZodDefault<z.ZodNumber>;
446
+ color: z.ZodDefault<z.ZodString>;
447
+ opacity: z.ZodDefault<z.ZodNumber>;
448
+ radius: z.ZodDefault<z.ZodNumber>;
449
+ }, z.core.$strip>>;
445
450
  padding: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{
446
451
  top: z.ZodDefault<z.ZodNumber>;
447
452
  right: z.ZodDefault<z.ZodNumber>;
@@ -492,13 +497,6 @@ declare const CanvasRichCaptionAssetSchema: z.ZodObject<{
492
497
  down: "down";
493
498
  }>>;
494
499
  }, z.core.$strict>>;
495
- position: z.ZodDefault<z.ZodEnum<{
496
- center: "center";
497
- top: "top";
498
- bottom: "bottom";
499
- }>>;
500
- maxWidth: z.ZodDefault<z.ZodNumber>;
501
- maxLines: z.ZodDefault<z.ZodNumber>;
502
500
  customFonts: z.ZodOptional<z.ZodArray<z.ZodObject<{
503
501
  src: z.ZodString;
504
502
  family: z.ZodString;
@@ -603,9 +601,11 @@ interface WordTiming {
603
601
  interface CaptionLayoutConfig {
604
602
  frameWidth: number;
605
603
  frameHeight: number;
606
- maxWidth: number;
604
+ availableWidth: number;
607
605
  maxLines: number;
608
- position: "top" | "center" | "bottom";
606
+ verticalAlign: "top" | "middle" | "bottom";
607
+ horizontalAlign: "left" | "center" | "right";
608
+ paddingLeft: number;
609
609
  fontSize: number;
610
610
  fontFamily: string;
611
611
  fontWeight: string | number;