@shotstack/shotstack-canvas 1.1.5 → 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,87 +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);
1209
- ctx.save();
1210
- ctx.fillRect(0, 0, canvas.width, canvas.height);
1211
- ctx.restore();
1213
+ offscreenCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
1214
+ const hasBackground = !!(op.bg && op.bg.color);
1215
+ needsAlphaExtraction = !hasBackground;
1212
1216
  if (op.bg && op.bg.color) {
1213
1217
  const { color, opacity, radius } = op.bg;
1214
- if (color) {
1215
- const c = parseHex6(color, opacity);
1216
- ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1217
- if (radius && radius > 0) {
1218
- ctx.save();
1219
- ctx.beginPath();
1220
- roundRectPath(ctx, 0, 0, op.width, op.height, radius);
1221
- ctx.fill();
1222
- ctx.restore();
1223
- } else {
1224
- ctx.fillRect(0, 0, op.width, op.height);
1225
- }
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);
1226
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);
1227
1234
  }
1228
1235
  continue;
1229
1236
  }
1237
+ const renderToBoth = (renderFn) => {
1238
+ if (needsAlphaExtraction) {
1239
+ renderFn(ctx);
1240
+ renderFn(offscreenCtx);
1241
+ } else {
1242
+ renderFn(ctx);
1243
+ }
1244
+ };
1230
1245
  if (op.op === "FillPath") {
1231
1246
  const fillOp = op;
1232
- ctx.save();
1233
- ctx.translate(fillOp.x, fillOp.y);
1234
- const s = fillOp.scale ?? 1;
1235
- ctx.scale(s, -s);
1236
- ctx.beginPath();
1237
- drawSvgPathOnCtx(ctx, fillOp.path);
1238
- const bbox = fillOp.gradientBBox ?? globalBox;
1239
- const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1240
- ctx.fillStyle = fill;
1241
- ctx.fill();
1242
- 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
+ });
1243
1260
  continue;
1244
1261
  }
1245
1262
  if (op.op === "StrokePath") {
1246
1263
  const strokeOp = op;
1247
- ctx.save();
1248
- ctx.translate(strokeOp.x, strokeOp.y);
1249
- const s = strokeOp.scale ?? 1;
1250
- ctx.scale(s, -s);
1251
- const invAbs = 1 / Math.abs(s);
1252
- ctx.beginPath();
1253
- drawSvgPathOnCtx(ctx, strokeOp.path);
1254
- const c = parseHex6(strokeOp.color, strokeOp.opacity);
1255
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1256
- ctx.lineWidth = strokeOp.width * invAbs;
1257
- ctx.lineJoin = "round";
1258
- ctx.lineCap = "round";
1259
- ctx.stroke();
1260
- 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
+ });
1261
1280
  continue;
1262
1281
  }
1263
1282
  if (op.op === "DecorationLine") {
1264
- ctx.save();
1265
- const c = parseHex6(op.color, op.opacity);
1266
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1267
- ctx.lineWidth = op.width;
1268
- ctx.beginPath();
1269
- ctx.moveTo(op.from.x, op.from.y);
1270
- ctx.lineTo(op.to.x, op.to.y);
1271
- ctx.stroke();
1272
- 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
+ });
1273
1294
  continue;
1274
1295
  }
1275
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
+ }
1276
1330
  },
1277
1331
  async toPNG() {
1278
1332
  return canvas.toBuffer("image/png");