@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.
@@ -1154,87 +1154,141 @@ async function createNodePainter(opts) {
1154
1154
  );
1155
1155
  const ctx = canvas.getContext("2d");
1156
1156
  if (!ctx) throw new Error("2D context unavailable in Node (canvas).");
1157
+ const offscreenCanvas = createCanvas(canvas.width, canvas.height);
1158
+ const offscreenCtx = offscreenCanvas.getContext("2d");
1157
1159
  const api = {
1158
1160
  async render(ops) {
1159
1161
  const globalBox = computeGlobalTextBounds(ops);
1162
+ let needsAlphaExtraction = false;
1160
1163
  for (const op of ops) {
1161
1164
  if (op.op === "BeginFrame") {
1162
- if (op.clear) ctx.clearRect(0, 0, op.width, op.height);
1163
1165
  const dpr = op.pixelRatio ?? opts.pixelRatio;
1164
1166
  const wantW = Math.floor(op.width * dpr);
1165
1167
  const wantH = Math.floor(op.height * dpr);
1166
1168
  if (canvas.width !== wantW || canvas.height !== wantH) {
1167
1169
  canvas.width = wantW;
1168
1170
  canvas.height = wantH;
1171
+ offscreenCanvas.width = wantW;
1172
+ offscreenCanvas.height = wantH;
1169
1173
  }
1170
1174
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1171
- ctx.save();
1172
- ctx.fillRect(0, 0, canvas.width, canvas.height);
1173
- ctx.restore();
1175
+ offscreenCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
1176
+ const hasBackground = !!(op.bg && op.bg.color);
1177
+ needsAlphaExtraction = !hasBackground;
1174
1178
  if (op.bg && op.bg.color) {
1175
1179
  const { color, opacity, radius } = op.bg;
1176
- if (color) {
1177
- const c = parseHex6(color, opacity);
1178
- ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1179
- if (radius && radius > 0) {
1180
- ctx.save();
1181
- ctx.beginPath();
1182
- roundRectPath(ctx, 0, 0, op.width, op.height, radius);
1183
- ctx.fill();
1184
- ctx.restore();
1185
- } else {
1186
- ctx.fillRect(0, 0, op.width, op.height);
1187
- }
1180
+ const c = parseHex6(color, opacity);
1181
+ ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1182
+ if (radius && radius > 0) {
1183
+ ctx.save();
1184
+ ctx.beginPath();
1185
+ roundRectPath(ctx, 0, 0, op.width, op.height, radius);
1186
+ ctx.fill();
1187
+ ctx.restore();
1188
+ } else {
1189
+ ctx.fillRect(0, 0, op.width, op.height);
1188
1190
  }
1191
+ } else {
1192
+ ctx.fillStyle = "rgb(255, 255, 255)";
1193
+ ctx.fillRect(0, 0, op.width, op.height);
1194
+ offscreenCtx.fillStyle = "rgb(0, 0, 0)";
1195
+ offscreenCtx.fillRect(0, 0, op.width, op.height);
1189
1196
  }
1190
1197
  continue;
1191
1198
  }
1199
+ const renderToBoth = (renderFn) => {
1200
+ if (needsAlphaExtraction) {
1201
+ renderFn(ctx);
1202
+ renderFn(offscreenCtx);
1203
+ } else {
1204
+ renderFn(ctx);
1205
+ }
1206
+ };
1192
1207
  if (op.op === "FillPath") {
1193
1208
  const fillOp = op;
1194
- ctx.save();
1195
- ctx.translate(fillOp.x, fillOp.y);
1196
- const s = fillOp.scale ?? 1;
1197
- ctx.scale(s, -s);
1198
- ctx.beginPath();
1199
- drawSvgPathOnCtx(ctx, fillOp.path);
1200
- const bbox = fillOp.gradientBBox ?? globalBox;
1201
- const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1202
- ctx.fillStyle = fill;
1203
- ctx.fill();
1204
- ctx.restore();
1209
+ renderToBoth((context) => {
1210
+ context.save();
1211
+ context.translate(fillOp.x, fillOp.y);
1212
+ const s = fillOp.scale ?? 1;
1213
+ context.scale(s, -s);
1214
+ context.beginPath();
1215
+ drawSvgPathOnCtx(context, fillOp.path);
1216
+ const bbox = fillOp.gradientBBox ?? globalBox;
1217
+ const fill = makeGradientFromBBox(context, fillOp.fill, bbox);
1218
+ context.fillStyle = fill;
1219
+ context.fill();
1220
+ context.restore();
1221
+ });
1205
1222
  continue;
1206
1223
  }
1207
1224
  if (op.op === "StrokePath") {
1208
1225
  const strokeOp = op;
1209
- ctx.save();
1210
- ctx.translate(strokeOp.x, strokeOp.y);
1211
- const s = strokeOp.scale ?? 1;
1212
- ctx.scale(s, -s);
1213
- const invAbs = 1 / Math.abs(s);
1214
- ctx.beginPath();
1215
- drawSvgPathOnCtx(ctx, strokeOp.path);
1216
- const c = parseHex6(strokeOp.color, strokeOp.opacity);
1217
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1218
- ctx.lineWidth = strokeOp.width * invAbs;
1219
- ctx.lineJoin = "round";
1220
- ctx.lineCap = "round";
1221
- ctx.stroke();
1222
- ctx.restore();
1226
+ renderToBoth((context) => {
1227
+ context.save();
1228
+ context.translate(strokeOp.x, strokeOp.y);
1229
+ const s = strokeOp.scale ?? 1;
1230
+ context.scale(s, -s);
1231
+ const invAbs = 1 / Math.abs(s);
1232
+ context.beginPath();
1233
+ drawSvgPathOnCtx(context, strokeOp.path);
1234
+ const c = parseHex6(strokeOp.color, strokeOp.opacity);
1235
+ context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1236
+ context.lineWidth = strokeOp.width * invAbs;
1237
+ context.lineJoin = "round";
1238
+ context.lineCap = "round";
1239
+ context.stroke();
1240
+ context.restore();
1241
+ });
1223
1242
  continue;
1224
1243
  }
1225
1244
  if (op.op === "DecorationLine") {
1226
- ctx.save();
1227
- const c = parseHex6(op.color, op.opacity);
1228
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1229
- ctx.lineWidth = op.width;
1230
- ctx.beginPath();
1231
- ctx.moveTo(op.from.x, op.from.y);
1232
- ctx.lineTo(op.to.x, op.to.y);
1233
- ctx.stroke();
1234
- ctx.restore();
1245
+ renderToBoth((context) => {
1246
+ context.save();
1247
+ const c = parseHex6(op.color, op.opacity);
1248
+ context.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1249
+ context.lineWidth = op.width;
1250
+ context.beginPath();
1251
+ context.moveTo(op.from.x, op.from.y);
1252
+ context.lineTo(op.to.x, op.to.y);
1253
+ context.stroke();
1254
+ context.restore();
1255
+ });
1235
1256
  continue;
1236
1257
  }
1237
1258
  }
1259
+ if (needsAlphaExtraction) {
1260
+ const whiteData = ctx.getImageData(0, 0, canvas.width, canvas.height);
1261
+ const blackData = offscreenCtx.getImageData(0, 0, canvas.width, canvas.height);
1262
+ const white = whiteData.data;
1263
+ const black = blackData.data;
1264
+ for (let i = 0; i < white.length; i += 4) {
1265
+ const wr = white[i];
1266
+ const wg = white[i + 1];
1267
+ const wb = white[i + 2];
1268
+ const br = black[i];
1269
+ const bg = black[i + 1];
1270
+ const bb = black[i + 2];
1271
+ const alpha = 255 - Math.max(wr - br, wg - bg, wb - bb);
1272
+ if (alpha <= 2) {
1273
+ white[i] = 0;
1274
+ white[i + 1] = 0;
1275
+ white[i + 2] = 0;
1276
+ white[i + 3] = 0;
1277
+ } else if (alpha >= 253) {
1278
+ white[i] = br;
1279
+ white[i + 1] = bg;
1280
+ white[i + 2] = bb;
1281
+ white[i + 3] = 255;
1282
+ } else {
1283
+ const alphaFactor = 255 / alpha;
1284
+ white[i] = Math.min(255, Math.max(0, Math.round(br * alphaFactor)));
1285
+ white[i + 1] = Math.min(255, Math.max(0, Math.round(bg * alphaFactor)));
1286
+ white[i + 2] = Math.min(255, Math.max(0, Math.round(bb * alphaFactor)));
1287
+ white[i + 3] = alpha;
1288
+ }
1289
+ }
1290
+ ctx.putImageData(whiteData, 0, 0);
1291
+ }
1238
1292
  },
1239
1293
  async toPNG() {
1240
1294
  return canvas.toBuffer("image/png");