@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.
@@ -1154,84 +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);
1175
+ offscreenCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
1176
+ const hasBackground = !!(op.bg && op.bg.color);
1177
+ needsAlphaExtraction = !hasBackground;
1171
1178
  if (op.bg && op.bg.color) {
1172
1179
  const { color, opacity, radius } = op.bg;
1173
- if (color) {
1174
- const c = parseHex6(color, opacity);
1175
- ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1176
- if (radius && radius > 0) {
1177
- ctx.save();
1178
- ctx.beginPath();
1179
- roundRectPath(ctx, 0, 0, op.width, op.height, radius);
1180
- ctx.fill();
1181
- ctx.restore();
1182
- } else {
1183
- ctx.fillRect(0, 0, op.width, op.height);
1184
- }
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);
1185
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);
1186
1196
  }
1187
1197
  continue;
1188
1198
  }
1199
+ const renderToBoth = (renderFn) => {
1200
+ if (needsAlphaExtraction) {
1201
+ renderFn(ctx);
1202
+ renderFn(offscreenCtx);
1203
+ } else {
1204
+ renderFn(ctx);
1205
+ }
1206
+ };
1189
1207
  if (op.op === "FillPath") {
1190
1208
  const fillOp = op;
1191
- ctx.save();
1192
- ctx.translate(fillOp.x, fillOp.y);
1193
- const s = fillOp.scale ?? 1;
1194
- ctx.scale(s, -s);
1195
- ctx.beginPath();
1196
- drawSvgPathOnCtx(ctx, fillOp.path);
1197
- const bbox = fillOp.gradientBBox ?? globalBox;
1198
- const fill = makeGradientFromBBox(ctx, fillOp.fill, bbox);
1199
- ctx.fillStyle = fill;
1200
- ctx.fill();
1201
- 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
+ });
1202
1222
  continue;
1203
1223
  }
1204
1224
  if (op.op === "StrokePath") {
1205
1225
  const strokeOp = op;
1206
- ctx.save();
1207
- ctx.translate(strokeOp.x, strokeOp.y);
1208
- const s = strokeOp.scale ?? 1;
1209
- ctx.scale(s, -s);
1210
- const invAbs = 1 / Math.abs(s);
1211
- ctx.beginPath();
1212
- drawSvgPathOnCtx(ctx, strokeOp.path);
1213
- const c = parseHex6(strokeOp.color, strokeOp.opacity);
1214
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1215
- ctx.lineWidth = strokeOp.width * invAbs;
1216
- ctx.lineJoin = "round";
1217
- ctx.lineCap = "round";
1218
- ctx.stroke();
1219
- 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
+ });
1220
1242
  continue;
1221
1243
  }
1222
1244
  if (op.op === "DecorationLine") {
1223
- ctx.save();
1224
- const c = parseHex6(op.color, op.opacity);
1225
- ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
1226
- ctx.lineWidth = op.width;
1227
- ctx.beginPath();
1228
- ctx.moveTo(op.from.x, op.from.y);
1229
- ctx.lineTo(op.to.x, op.to.y);
1230
- ctx.stroke();
1231
- 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
+ });
1232
1256
  continue;
1233
1257
  }
1234
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
+ }
1235
1292
  },
1236
1293
  async toPNG() {
1237
1294
  return canvas.toBuffer("image/png");