@shotstack/shotstack-canvas 1.1.7 → 1.1.9

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.
@@ -204,6 +204,7 @@ interface VideoGenerationOptions {
204
204
  duration: number;
205
205
  outputPath: string;
206
206
  pixelRatio?: number;
207
+ hasAlpha?: boolean;
207
208
  crf?: number;
208
209
  preset?: "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow";
209
210
  tune?: "film" | "animation" | "grain" | "stillimage" | "psnr" | "ssim" | "fastdecode" | "zerolatency";
@@ -204,6 +204,7 @@ interface VideoGenerationOptions {
204
204
  duration: number;
205
205
  outputPath: string;
206
206
  pixelRatio?: number;
207
+ hasAlpha?: boolean;
207
208
  crf?: number;
208
209
  preset?: "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow";
209
210
  tune?: "film" | "animation" | "grain" | "stillimage" | "psnr" | "ssim" | "fastdecode" | "zerolatency";
@@ -1210,6 +1210,7 @@ async function createNodePainter(opts) {
1210
1210
  if (!ctx) throw new Error("2D context unavailable in Node (canvas).");
1211
1211
  const offscreenCanvas = createCanvas(canvas.width, canvas.height);
1212
1212
  const offscreenCtx = offscreenCanvas.getContext("2d");
1213
+ const gradientCache = /* @__PURE__ */ new Map();
1213
1214
  const api = {
1214
1215
  async render(ops) {
1215
1216
  const globalBox = computeGlobalTextBounds(ops);
@@ -1230,6 +1231,7 @@ async function createNodePainter(opts) {
1230
1231
  const hasBackground = !!(op.bg && op.bg.color);
1231
1232
  needsAlphaExtraction = !hasBackground;
1232
1233
  if (op.bg && op.bg.color) {
1234
+ ctx.clearRect(0, 0, op.width, op.height);
1233
1235
  const { color, opacity, radius } = op.bg;
1234
1236
  const c = parseHex6(color, opacity);
1235
1237
  ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
@@ -1243,10 +1245,15 @@ async function createNodePainter(opts) {
1243
1245
  ctx.fillRect(0, 0, op.width, op.height);
1244
1246
  }
1245
1247
  } else {
1248
+ ctx.clearRect(0, 0, op.width, op.height);
1249
+ ctx.save();
1246
1250
  ctx.fillStyle = "rgb(255, 255, 255)";
1247
1251
  ctx.fillRect(0, 0, op.width, op.height);
1252
+ ctx.restore();
1253
+ offscreenCtx.save();
1248
1254
  offscreenCtx.fillStyle = "rgb(0, 0, 0)";
1249
1255
  offscreenCtx.fillRect(0, 0, op.width, op.height);
1256
+ offscreenCtx.restore();
1250
1257
  }
1251
1258
  continue;
1252
1259
  }
@@ -1265,11 +1272,23 @@ async function createNodePainter(opts) {
1265
1272
  context.translate(fillOp.x, fillOp.y);
1266
1273
  const s = fillOp.scale ?? 1;
1267
1274
  context.scale(s, -s);
1268
- context.beginPath();
1269
- drawSvgPathOnCtx(context, fillOp.path);
1270
1275
  const bbox = fillOp.gradientBBox ?? globalBox;
1271
- const fill = makeGradientFromBBox(context, fillOp.fill, bbox);
1276
+ const localBBox = {
1277
+ x: (bbox.x - fillOp.x) / s,
1278
+ y: -(bbox.y - fillOp.y) / s,
1279
+ w: bbox.w / s,
1280
+ h: bbox.h / s
1281
+ };
1282
+ const cacheKey = JSON.stringify({ fill: fillOp.fill, bbox: localBBox });
1283
+ let fill = gradientCache.get(cacheKey);
1284
+ if (!fill) {
1285
+ fill = makeGradientFromBBox(context, fillOp.fill, localBBox);
1286
+ gradientCache.set(cacheKey, fill);
1287
+ console.log("[Node Painter] Created NEW gradient for local bbox:", localBBox);
1288
+ }
1272
1289
  context.fillStyle = fill;
1290
+ context.beginPath();
1291
+ drawSvgPathOnCtx(context, fillOp.path);
1273
1292
  context.fill();
1274
1293
  context.restore();
1275
1294
  });
@@ -1632,44 +1651,72 @@ var VideoGenerator = class {
1632
1651
  tune = "animation",
1633
1652
  profile = "high",
1634
1653
  level = "4.2",
1635
- pixFmt = "yuv420p"
1654
+ pixFmt = "yuv420p",
1655
+ hasAlpha = false
1636
1656
  } = options;
1637
1657
  const totalFrames = Math.max(2, Math.round(duration * fps) + 1);
1658
+ const finalOutputPath = hasAlpha ? outputPath.replace(/\.mp4$/, ".mov") : outputPath;
1638
1659
  console.log(
1639
- `\u{1F3AC} Generating video: ${width}x${height} @ ${fps}fps, ${duration}s (${totalFrames} frames)
1640
- CRF=${crf}, preset=${preset}, tune=${tune}, profile=${profile}, level=${level}, pix_fmt=${pixFmt}`
1660
+ `\u{1F3AC} Generating video: ${width}x${height} @ ${fps}fps, ${duration}s (${totalFrames} frames)` + (hasAlpha ? " (MOV with alpha)" : "")
1641
1661
  );
1642
1662
  return new Promise(async (resolve, reject) => {
1643
- const args = [
1644
- "-y",
1645
- "-f",
1646
- "image2pipe",
1647
- "-vcodec",
1648
- "png",
1649
- "-framerate",
1650
- String(fps),
1651
- "-i",
1652
- "-",
1653
- "-c:v",
1654
- "libx264",
1655
- "-preset",
1656
- preset,
1657
- "-crf",
1658
- String(crf),
1659
- "-tune",
1660
- tune,
1661
- "-profile:v",
1662
- profile,
1663
- "-level",
1664
- level,
1665
- "-pix_fmt",
1666
- pixFmt,
1667
- "-r",
1668
- String(fps),
1669
- "-movflags",
1670
- "+faststart",
1671
- outputPath
1672
- ];
1663
+ let args;
1664
+ if (hasAlpha) {
1665
+ args = [
1666
+ "-y",
1667
+ "-f",
1668
+ "image2pipe",
1669
+ "-vcodec",
1670
+ "png",
1671
+ "-framerate",
1672
+ String(fps),
1673
+ "-i",
1674
+ "-",
1675
+ "-c:v",
1676
+ "prores_ks",
1677
+ "-profile:v",
1678
+ "4444",
1679
+ "-pix_fmt",
1680
+ "yuva444p10le",
1681
+ "-vendor",
1682
+ "apl0",
1683
+ "-r",
1684
+ String(fps),
1685
+ finalOutputPath
1686
+ ];
1687
+ console.log(` Output: ${finalOutputPath} (ProRes 4444 with alpha)`);
1688
+ } else {
1689
+ args = [
1690
+ "-y",
1691
+ "-f",
1692
+ "image2pipe",
1693
+ "-vcodec",
1694
+ "png",
1695
+ "-framerate",
1696
+ String(fps),
1697
+ "-i",
1698
+ "-",
1699
+ "-c:v",
1700
+ "libx264",
1701
+ "-preset",
1702
+ preset,
1703
+ "-crf",
1704
+ String(crf),
1705
+ "-tune",
1706
+ tune,
1707
+ "-profile:v",
1708
+ profile,
1709
+ "-level",
1710
+ level,
1711
+ "-pix_fmt",
1712
+ pixFmt,
1713
+ "-r",
1714
+ String(fps),
1715
+ "-movflags",
1716
+ "+faststart",
1717
+ outputPath
1718
+ ];
1719
+ }
1673
1720
  const ffmpeg = spawn(this.ffmpegPath, args, { stdio: ["pipe", "inherit", "pipe"] });
1674
1721
  let ffmpegError = "";
1675
1722
  ffmpeg.stderr.on("data", (data) => {
@@ -1918,13 +1965,21 @@ async function createTextEngine(opts = {}) {
1918
1965
  },
1919
1966
  async generateVideo(asset, options) {
1920
1967
  try {
1968
+ const hasBackground = !!asset.background?.color;
1969
+ const hasAnimation = !!asset.animation?.preset;
1970
+ const needsAlpha = !hasBackground && hasAnimation;
1971
+ console.log(
1972
+ `\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, Alpha=${needsAlpha}`
1973
+ );
1921
1974
  const finalOptions = {
1922
1975
  width: asset.width ?? width,
1923
1976
  height: asset.height ?? height,
1924
1977
  fps,
1925
1978
  duration: asset.animation?.duration ?? 3,
1926
1979
  outputPath: options.outputPath ?? "output.mp4",
1927
- pixelRatio: asset.pixelRatio ?? pixelRatio
1980
+ pixelRatio: asset.pixelRatio ?? pixelRatio,
1981
+ hasAlpha: needsAlpha,
1982
+ ...options
1928
1983
  };
1929
1984
  const frameGenerator = async (time) => {
1930
1985
  return this.renderFrame(asset, time);