@shotstack/shotstack-canvas 1.1.4 → 1.1.6

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.
@@ -1192,84 +1192,141 @@ async function createNodePainter(opts) {
1192
1192
  );
1193
1193
  const ctx = canvas.getContext("2d");
1194
1194
  if (!ctx) throw new Error("2D context unavailable in Node (canvas).");
1195
+ const offscreenCanvas = createCanvas(canvas.width, canvas.height);
1196
+ const offscreenCtx = offscreenCanvas.getContext("2d");
1195
1197
  const api = {
1196
1198
  async render(ops) {
1197
1199
  const globalBox = computeGlobalTextBounds(ops);
1200
+ let needsAlphaExtraction = false;
1198
1201
  for (const op of ops) {
1199
1202
  if (op.op === "BeginFrame") {
1200
- if (op.clear) ctx.clearRect(0, 0, op.width, op.height);
1201
1203
  const dpr = op.pixelRatio ?? opts.pixelRatio;
1202
1204
  const wantW = Math.floor(op.width * dpr);
1203
1205
  const wantH = Math.floor(op.height * dpr);
1204
1206
  if (canvas.width !== wantW || canvas.height !== wantH) {
1205
1207
  canvas.width = wantW;
1206
1208
  canvas.height = wantH;
1209
+ offscreenCanvas.width = wantW;
1210
+ offscreenCanvas.height = wantH;
1207
1211
  }
1208
1212
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1213
+ offscreenCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
1214
+ const hasBackground = !!(op.bg && op.bg.color);
1215
+ needsAlphaExtraction = !hasBackground;
1209
1216
  if (op.bg && op.bg.color) {
1210
1217
  const { color, opacity, radius } = op.bg;
1211
- if (color) {
1212
- const c = parseHex6(color, opacity);
1213
- ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1214
- if (radius && radius > 0) {
1215
- ctx.save();
1216
- ctx.beginPath();
1217
- roundRectPath(ctx, 0, 0, op.width, op.height, radius);
1218
- ctx.fill();
1219
- ctx.restore();
1220
- } else {
1221
- ctx.fillRect(0, 0, op.width, op.height);
1222
- }
1218
+ const c = parseHex6(color, opacity);
1219
+ ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1220
+ if (radius && radius > 0) {
1221
+ ctx.save();
1222
+ ctx.beginPath();
1223
+ roundRectPath(ctx, 0, 0, op.width, op.height, radius);
1224
+ ctx.fill();
1225
+ ctx.restore();
1226
+ } else {
1227
+ ctx.fillRect(0, 0, op.width, op.height);
1223
1228
  }
1229
+ } else {
1230
+ ctx.fillStyle = "rgb(255, 255, 255)";
1231
+ ctx.fillRect(0, 0, op.width, op.height);
1232
+ offscreenCtx.fillStyle = "rgb(0, 0, 0)";
1233
+ offscreenCtx.fillRect(0, 0, op.width, op.height);
1224
1234
  }
1225
1235
  continue;
1226
1236
  }
1237
+ const renderToBoth = (renderFn) => {
1238
+ if (needsAlphaExtraction) {
1239
+ renderFn(ctx);
1240
+ renderFn(offscreenCtx);
1241
+ } else {
1242
+ renderFn(ctx);
1243
+ }
1244
+ };
1227
1245
  if (op.op === "FillPath") {
1228
1246
  const fillOp = op;
1229
- ctx.save();
1230
- ctx.translate(fillOp.x, fillOp.y);
1231
- const s = fillOp.scale ?? 1;
1232
- ctx.scale(s, -s);
1233
- ctx.beginPath();
1234
- drawSvgPathOnCtx(ctx, fillOp.path);
1235
- const bbox = fillOp.gradientBBox ?? globalBox;
1236
- const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1237
- ctx.fillStyle = fill;
1238
- ctx.fill();
1239
- ctx.restore();
1247
+ renderToBoth((context) => {
1248
+ context.save();
1249
+ context.translate(fillOp.x, fillOp.y);
1250
+ const s = fillOp.scale ?? 1;
1251
+ context.scale(s, -s);
1252
+ context.beginPath();
1253
+ drawSvgPathOnCtx(context, fillOp.path);
1254
+ const bbox = fillOp.gradientBBox ?? globalBox;
1255
+ const fill = makeGradientFromBBox(context, fillOp.fill, bbox);
1256
+ context.fillStyle = fill;
1257
+ context.fill();
1258
+ context.restore();
1259
+ });
1240
1260
  continue;
1241
1261
  }
1242
1262
  if (op.op === "StrokePath") {
1243
1263
  const strokeOp = op;
1244
- ctx.save();
1245
- ctx.translate(strokeOp.x, strokeOp.y);
1246
- const s = strokeOp.scale ?? 1;
1247
- ctx.scale(s, -s);
1248
- const invAbs = 1 / Math.abs(s);
1249
- ctx.beginPath();
1250
- drawSvgPathOnCtx(ctx, strokeOp.path);
1251
- const c = parseHex6(strokeOp.color, strokeOp.opacity);
1252
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1253
- ctx.lineWidth = strokeOp.width * invAbs;
1254
- ctx.lineJoin = "round";
1255
- ctx.lineCap = "round";
1256
- ctx.stroke();
1257
- ctx.restore();
1264
+ renderToBoth((context) => {
1265
+ context.save();
1266
+ context.translate(strokeOp.x, strokeOp.y);
1267
+ const s = strokeOp.scale ?? 1;
1268
+ context.scale(s, -s);
1269
+ const invAbs = 1 / Math.abs(s);
1270
+ context.beginPath();
1271
+ drawSvgPathOnCtx(context, strokeOp.path);
1272
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1273
+ context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1274
+ context.lineWidth = strokeOp.width * invAbs;
1275
+ context.lineJoin = "round";
1276
+ context.lineCap = "round";
1277
+ context.stroke();
1278
+ context.restore();
1279
+ });
1258
1280
  continue;
1259
1281
  }
1260
1282
  if (op.op === "DecorationLine") {
1261
- ctx.save();
1262
- const c = parseHex6(op.color, op.opacity);
1263
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1264
- ctx.lineWidth = op.width;
1265
- ctx.beginPath();
1266
- ctx.moveTo(op.from.x, op.from.y);
1267
- ctx.lineTo(op.to.x, op.to.y);
1268
- ctx.stroke();
1269
- ctx.restore();
1283
+ renderToBoth((context) => {
1284
+ context.save();
1285
+ const c = parseHex6(op.color, op.opacity);
1286
+ context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1287
+ context.lineWidth = op.width;
1288
+ context.beginPath();
1289
+ context.moveTo(op.from.x, op.from.y);
1290
+ context.lineTo(op.to.x, op.to.y);
1291
+ context.stroke();
1292
+ context.restore();
1293
+ });
1270
1294
  continue;
1271
1295
  }
1272
1296
  }
1297
+ if (needsAlphaExtraction) {
1298
+ const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
1299
+ const blackData = offscreenCtx.getImageData(0, 0, canvas.width, canvas.height);
1300
+ const white = whiteData.data;
1301
+ const black = blackData.data;
1302
+ for (let i = 0; i < white.length; i += 4) {
1303
+ const wr = white[i];
1304
+ const wg = white[i + 1];
1305
+ const wb = white[i + 2];
1306
+ const br = black[i];
1307
+ const bg = black[i + 1];
1308
+ const bb = black[i + 2];
1309
+ const alpha = 255 - Math.max(wr - br, wg - bg, wb - bb);
1310
+ if (alpha <= 2) {
1311
+ white[i] = 0;
1312
+ white[i + 1] = 0;
1313
+ white[i + 2] = 0;
1314
+ white[i + 3] = 0;
1315
+ } else if (alpha >= 253) {
1316
+ white[i] = br;
1317
+ white[i + 1] = bg;
1318
+ white[i + 2] = bb;
1319
+ white[i + 3] = 255;
1320
+ } else {
1321
+ const alphaFactor = 255 / alpha;
1322
+ white[i] = Math.min(255, Math.max(0, Math.round(br * alphaFactor)));
1323
+ white[i + 1] = Math.min(255, Math.max(0, Math.round(bg * alphaFactor)));
1324
+ white[i + 2] = Math.min(255, Math.max(0, Math.round(bb * alphaFactor)));
1325
+ white[i + 3] = alpha;
1326
+ }
1327
+ }
1328
+ ctx.putImageData(whiteData, 0, 0);
1329
+ }
1273
1330
  },
1274
1331
  async toPNG() {
1275
1332
  return canvas.toBuffer("image/png");