@shotstack/shotstack-canvas 2.0.8 → 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.
package/dist/entry.web.js CHANGED
@@ -17897,8 +17897,7 @@ import {
17897
17897
  svgTransformSchema,
17898
17898
  svgGradientStopSchema,
17899
17899
  richCaptionActiveSchema as baseCaptionActiveSchema,
17900
- richCaptionWordAnimationSchema as baseCaptionWordAnimationSchema,
17901
- wordTimingSchema as baseWordTimingSchema
17900
+ richCaptionWordAnimationSchema as baseCaptionWordAnimationSchema
17902
17901
  } from "@shotstack/schemas/zod";
17903
17902
 
17904
17903
  // src/config/canvas-constants.ts
@@ -18055,7 +18054,7 @@ var CanvasRichTextAssetSchema = richTextAssetSchema.extend({
18055
18054
  customFonts: external_exports.array(customFontSchema).optional()
18056
18055
  }).strict();
18057
18056
  var CanvasSvgAssetSchema = svgAssetSchema;
18058
- var wordTimingSchema = baseWordTimingSchema.extend({
18057
+ var wordTimingSchema = external_exports.object({
18059
18058
  text: external_exports.string().min(1),
18060
18059
  start: external_exports.number().min(0),
18061
18060
  end: external_exports.number().min(0),
@@ -18096,27 +18095,18 @@ var richCaptionAssetSchema = external_exports.object({
18096
18095
  stroke: canvasStrokeSchema.optional(),
18097
18096
  shadow: canvasShadowSchema.optional(),
18098
18097
  background: canvasBackgroundSchema.optional(),
18098
+ border: borderSchema.optional(),
18099
18099
  padding: paddingSchema.optional(),
18100
18100
  align: canvasAlignmentSchema.optional(),
18101
18101
  active: richCaptionActiveSchema.optional(),
18102
18102
  wordAnimation: richCaptionWordAnimationSchema.optional(),
18103
- position: external_exports.enum(["top", "center", "bottom"]).default("bottom"),
18104
- maxWidth: external_exports.number().min(0.1).max(1).default(0.9),
18105
- maxLines: external_exports.number().int().min(1).max(10).default(2),
18106
18103
  customFonts: external_exports.array(customFontSchema).optional()
18107
18104
  }).superRefine((data, ctx) => {
18108
- if (data.src && data.words) {
18109
- ctx.addIssue({
18110
- code: external_exports.ZodIssueCode.custom,
18111
- message: "src and words are mutually exclusive",
18112
- path: ["src"]
18113
- });
18114
- }
18115
18105
  if (!data.src && !data.words) {
18116
18106
  ctx.addIssue({
18117
18107
  code: external_exports.ZodIssueCode.custom,
18118
18108
  message: "Either src or words must be provided",
18119
- path: ["words"]
18109
+ path: ["src"]
18120
18110
  });
18121
18111
  }
18122
18112
  });
@@ -35624,7 +35614,7 @@ var CaptionLayoutEngine = class {
35624
35614
  }
35625
35615
  }
35626
35616
  const wordGroups = groupWordsByPause(store, config2.pauseThreshold);
35627
- const pixelMaxWidth = config2.frameWidth * config2.maxWidth;
35617
+ const pixelMaxWidth = config2.availableWidth;
35628
35618
  let spaceWidth;
35629
35619
  if (config2.measureTextWidth) {
35630
35620
  const fontString = `${config2.fontWeight} ${config2.fontSize}px "${config2.fontFamily}"`;
@@ -35654,31 +35644,45 @@ var CaptionLayoutEngine = class {
35654
35644
  };
35655
35645
  });
35656
35646
  const allWordIndices = lines.flatMap((l) => l.wordIndices);
35647
+ if (allWordIndices.length === 0) {
35648
+ return null;
35649
+ }
35657
35650
  return {
35658
35651
  wordIndices: allWordIndices,
35659
35652
  startTime: store.startTimes[allWordIndices[0]],
35660
35653
  endTime: store.endTimes[allWordIndices[allWordIndices.length - 1]],
35661
35654
  lines
35662
35655
  };
35663
- });
35656
+ }).filter((g) => g !== null);
35664
35657
  });
35665
35658
  const calculateGroupY = (group) => {
35666
35659
  const totalHeight = group.lines.length * config2.fontSize * config2.lineHeight;
35667
- switch (config2.position) {
35660
+ switch (config2.verticalAlign) {
35668
35661
  case "top":
35669
35662
  return config2.fontSize * 1.5;
35670
35663
  case "bottom":
35671
35664
  return config2.frameHeight - totalHeight - config2.fontSize * 0.5;
35672
- case "center":
35665
+ case "middle":
35673
35666
  default:
35674
35667
  return (config2.frameHeight - totalHeight) / 2 + config2.fontSize;
35675
35668
  }
35676
35669
  };
35670
+ const calculateLineX = (lineWidth) => {
35671
+ switch (config2.horizontalAlign) {
35672
+ case "left":
35673
+ return config2.paddingLeft;
35674
+ case "right":
35675
+ return config2.frameWidth - lineWidth - config2.paddingLeft;
35676
+ case "center":
35677
+ default:
35678
+ return (config2.frameWidth - lineWidth) / 2;
35679
+ }
35680
+ };
35677
35681
  for (const group of groups) {
35678
35682
  const baseY = calculateGroupY(group);
35679
35683
  for (let lineIdx = 0; lineIdx < group.lines.length; lineIdx++) {
35680
35684
  const line = group.lines[lineIdx];
35681
- line.x = (config2.frameWidth - line.width) / 2;
35685
+ line.x = calculateLineX(line.width);
35682
35686
  line.y = baseY + lineIdx * config2.fontSize * config2.lineHeight;
35683
35687
  let xCursor = line.x;
35684
35688
  for (const wordIdx of line.wordIndices) {
@@ -35969,6 +35973,7 @@ function calculateNoneState(ctx) {
35969
35973
  };
35970
35974
  }
35971
35975
  function calculateWordAnimationState(wordStart, wordEnd, currentTime, config2, activeScale = 1, charCount = 0, fontSize = 48) {
35976
+ const safeSpeed = config2.speed > 0 ? config2.speed : 1;
35972
35977
  const ctx = {
35973
35978
  wordStart,
35974
35979
  wordEnd,
@@ -35979,25 +35984,25 @@ function calculateWordAnimationState(wordStart, wordEnd, currentTime, config2, a
35979
35984
  let partialState;
35980
35985
  switch (config2.style) {
35981
35986
  case "karaoke":
35982
- partialState = calculateKaraokeState(ctx, config2.speed);
35987
+ partialState = calculateKaraokeState(ctx, safeSpeed);
35983
35988
  break;
35984
35989
  case "highlight":
35985
35990
  partialState = calculateHighlightState(ctx);
35986
35991
  break;
35987
35992
  case "pop":
35988
- partialState = calculatePopState(ctx, activeScale, config2.speed);
35993
+ partialState = calculatePopState(ctx, activeScale, safeSpeed);
35989
35994
  break;
35990
35995
  case "fade":
35991
- partialState = calculateFadeState(ctx, config2.speed);
35996
+ partialState = calculateFadeState(ctx, safeSpeed);
35992
35997
  break;
35993
35998
  case "slide":
35994
- partialState = calculateSlideState(ctx, config2.direction, config2.speed, fontSize);
35999
+ partialState = calculateSlideState(ctx, config2.direction, safeSpeed, fontSize);
35995
36000
  break;
35996
36001
  case "bounce":
35997
- partialState = calculateBounceState(ctx, config2.speed, fontSize);
36002
+ partialState = calculateBounceState(ctx, safeSpeed, fontSize);
35998
36003
  break;
35999
36004
  case "typewriter":
36000
- partialState = calculateTypewriterState(ctx, charCount, config2.speed);
36005
+ partialState = calculateTypewriterState(ctx, charCount, safeSpeed);
36001
36006
  break;
36002
36007
  case "none":
36003
36008
  default:
@@ -36121,6 +36126,22 @@ function extractCaptionBackground(asset) {
36121
36126
  opacity: bg.opacity ?? 1
36122
36127
  };
36123
36128
  }
36129
+ function extractCaptionBackgroundBorderRadius(asset) {
36130
+ const bg = asset.background;
36131
+ return bg?.borderRadius ?? 0;
36132
+ }
36133
+ function extractCaptionBorder(asset) {
36134
+ const border = asset.border;
36135
+ if (!border || !border.width || border.width <= 0) {
36136
+ return void 0;
36137
+ }
36138
+ return {
36139
+ width: border.width,
36140
+ color: border.color ?? "#000000",
36141
+ opacity: border.opacity ?? 1,
36142
+ radius: border.radius ?? 0
36143
+ };
36144
+ }
36124
36145
  function extractAnimationConfig(asset) {
36125
36146
  const wordAnim = asset.wordAnimation;
36126
36147
  if (!wordAnim) {
@@ -36166,6 +36187,28 @@ function createDrawCaptionWordOp(word, animState, asset, fontConfig) {
36166
36187
  background: extractBackgroundConfig(asset, isActive)
36167
36188
  };
36168
36189
  }
36190
+ function calculateGroupBounds(activeGroup, padding) {
36191
+ let minX = Infinity;
36192
+ let maxX = -Infinity;
36193
+ let minY = Infinity;
36194
+ let maxY = -Infinity;
36195
+ for (const line of activeGroup.lines) {
36196
+ const lineX = line.x;
36197
+ const lineRight = line.x + line.width;
36198
+ const lineY = line.y - line.height * 0.8;
36199
+ const lineBottom = line.y + line.height * 0.2;
36200
+ if (lineX < minX) minX = lineX;
36201
+ if (lineRight > maxX) maxX = lineRight;
36202
+ if (lineY < minY) minY = lineY;
36203
+ if (lineBottom > maxY) maxY = lineBottom;
36204
+ }
36205
+ return {
36206
+ bgX: minX - padding.left,
36207
+ bgY: minY - padding.top,
36208
+ bgWidth: maxX - minX + padding.left + padding.right,
36209
+ bgHeight: maxY - minY + padding.top + padding.bottom
36210
+ };
36211
+ }
36169
36212
  function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, _config) {
36170
36213
  if (layout.store.length === 0) {
36171
36214
  return [];
@@ -36185,36 +36228,40 @@ function generateRichCaptionDrawOps(asset, layout, frameTimeMs, layoutEngine, _c
36185
36228
  fontConfig.size
36186
36229
  );
36187
36230
  const ops = [];
36188
- const captionBg = extractCaptionBackground(asset);
36189
- if (captionBg) {
36190
- const activeGroup = layout.groups.find(
36191
- (g) => frameTimeMs >= g.startTime && frameTimeMs <= g.endTime
36192
- );
36193
- if (activeGroup && activeGroup.lines.length > 0) {
36194
- const padding = extractCaptionPadding(asset);
36195
- let minX = Infinity;
36196
- let maxX = -Infinity;
36197
- let minY = Infinity;
36198
- let maxY = -Infinity;
36199
- for (const line of activeGroup.lines) {
36200
- const lineX = line.x;
36201
- const lineRight = line.x + line.width;
36202
- const lineY = line.y - line.height * 0.8;
36203
- const lineBottom = line.y + line.height * 0.2;
36204
- if (lineX < minX) minX = lineX;
36205
- if (lineRight > maxX) maxX = lineRight;
36206
- if (lineY < minY) minY = lineY;
36207
- if (lineBottom > maxY) maxY = lineBottom;
36208
- }
36231
+ const activeGroup = layout.groups.find(
36232
+ (g) => frameTimeMs >= g.startTime && frameTimeMs <= g.endTime
36233
+ );
36234
+ if (activeGroup && activeGroup.lines.length > 0) {
36235
+ const padding = extractCaptionPadding(asset);
36236
+ const { bgX, bgY, bgWidth, bgHeight } = calculateGroupBounds(activeGroup, padding);
36237
+ const captionBg = extractCaptionBackground(asset);
36238
+ if (captionBg) {
36209
36239
  ops.push({
36210
36240
  op: "DrawCaptionBackground",
36211
- x: minX - padding.left,
36212
- y: minY - padding.top,
36213
- width: maxX - minX + padding.left + padding.right,
36214
- height: maxY - minY + padding.top + padding.bottom,
36241
+ x: bgX,
36242
+ y: bgY,
36243
+ width: bgWidth,
36244
+ height: bgHeight,
36215
36245
  color: captionBg.color,
36216
36246
  opacity: captionBg.opacity,
36217
- borderRadius: 8
36247
+ borderRadius: extractCaptionBackgroundBorderRadius(asset)
36248
+ });
36249
+ }
36250
+ const borderConfig = extractCaptionBorder(asset);
36251
+ if (borderConfig) {
36252
+ const halfBorder = borderConfig.width / 2;
36253
+ ops.push({
36254
+ op: "RectangleStroke",
36255
+ x: bgX + halfBorder,
36256
+ y: bgY + halfBorder,
36257
+ width: bgWidth - borderConfig.width,
36258
+ height: bgHeight - borderConfig.width,
36259
+ stroke: {
36260
+ width: borderConfig.width,
36261
+ color: borderConfig.color,
36262
+ opacity: borderConfig.opacity
36263
+ },
36264
+ borderRadius: borderConfig.radius > 0 ? borderConfig.radius : void 0
36218
36265
  });
36219
36266
  }
36220
36267
  }
package/package.json CHANGED
@@ -1,65 +1,65 @@
1
- {
2
- "name": "@shotstack/shotstack-canvas",
3
- "version": "2.0.8",
4
- "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
- "type": "module",
6
- "main": "./dist/entry.node.cjs",
7
- "module": "./dist/entry.node.js",
8
- "browser": "./dist/entry.web.js",
9
- "types": "./dist/entry.node.d.ts",
10
- "exports": {
11
- ".": {
12
- "node": {
13
- "import": "./dist/entry.node.js",
14
- "require": "./dist/entry.node.cjs"
15
- },
16
- "browser": "./dist/entry.web.js",
17
- "default": "./dist/entry.web.js"
18
- }
19
- },
20
- "files": [
21
- "dist/**",
22
- "scripts/postinstall.js",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "scripts": {
27
- "dev": "tsup --watch",
28
- "build": "tsup",
29
- "postinstall": "node scripts/postinstall.js",
30
- "vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
31
- "example:node": "node examples/node-example.mjs",
32
- "example:video": "node examples/node-video.mjs",
33
- "example:web": "vite dev examples/web-example",
34
- "test:caption-web": "vite dev examples/caption-tests",
35
- "prepublishOnly": "npm run build"
36
- },
37
- "publishConfig": {
38
- "access": "public",
39
- "registry": "https://registry.npmjs.org/"
40
- },
41
- "engines": {
42
- "node": ">=18"
43
- },
44
- "sideEffects": false,
45
- "dependencies": {
46
- "@resvg/resvg-js": "^2.6.2",
47
- "@resvg/resvg-wasm": "^2.6.2",
48
- "@shotstack/schemas": "1.8.5",
49
- "canvas": "npm:@napi-rs/canvas@^0.1.54",
50
- "ffmpeg-static": "^5.2.0",
51
- "fontkit": "^2.0.4",
52
- "harfbuzzjs": "0.4.12",
53
- "lru-cache": "^11.2.5",
54
- "mp4-muxer": "^5.1.3",
55
- "zod": "^4.2.0"
56
- },
57
- "devDependencies": {
58
- "@types/node": "^20.14.10",
59
- "tsup": "^8.2.3",
60
- "typescript": "^5.5.3",
61
- "vite": "^5.3.3",
62
- "vite-plugin-top-level-await": "1.6.0",
63
- "vite-plugin-wasm": "3.5.0"
64
- }
65
- }
1
+ {
2
+ "name": "@shotstack/shotstack-canvas",
3
+ "version": "2.0.10",
4
+ "description": "Text layout & animation engine (HarfBuzz) for Node & Web - fully self-contained.",
5
+ "type": "module",
6
+ "main": "./dist/entry.node.cjs",
7
+ "module": "./dist/entry.node.js",
8
+ "browser": "./dist/entry.web.js",
9
+ "types": "./dist/entry.node.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "node": {
13
+ "import": "./dist/entry.node.js",
14
+ "require": "./dist/entry.node.cjs"
15
+ },
16
+ "browser": "./dist/entry.web.js",
17
+ "default": "./dist/entry.web.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist/**",
22
+ "scripts/postinstall.js",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "dev": "tsup --watch",
28
+ "build": "tsup",
29
+ "postinstall": "node scripts/postinstall.js",
30
+ "vendor:harfbuzz": "node scripts/vendor-harfbuzz.js",
31
+ "example:node": "node examples/node-example.mjs",
32
+ "example:video": "node examples/node-video.mjs",
33
+ "example:web": "vite dev examples/web-example",
34
+ "test:caption-web": "vite dev examples/caption-tests",
35
+ "prepublishOnly": "npm run build"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "sideEffects": false,
45
+ "dependencies": {
46
+ "@resvg/resvg-js": "^2.6.2",
47
+ "@resvg/resvg-wasm": "^2.6.2",
48
+ "@shotstack/schemas": "1.8.7",
49
+ "canvas": "npm:@napi-rs/canvas@^0.1.54",
50
+ "ffmpeg-static": "^5.2.0",
51
+ "fontkit": "^2.0.4",
52
+ "harfbuzzjs": "0.4.12",
53
+ "lru-cache": "^11.2.5",
54
+ "mp4-muxer": "^5.1.3",
55
+ "zod": "^4.2.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^20.14.10",
59
+ "tsup": "^8.2.3",
60
+ "typescript": "^5.5.3",
61
+ "vite": "^5.3.3",
62
+ "vite-plugin-top-level-await": "1.6.0",
63
+ "vite-plugin-wasm": "3.5.0"
64
+ }
65
+ }