@pixldocs/canvas-renderer 0.5.101 → 0.5.102

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.
package/dist/index.js CHANGED
@@ -3663,28 +3663,33 @@ function updateCoverLayout(g) {
3663
3663
  const hasCustomSvgMask = Boolean(
3664
3664
  currentClipPath && (isSvgMaskClipPath(currentClipPath) || isLuminanceMaskClipPath(currentClipPath) || currentClipPath.__svgMask)
3665
3665
  );
3666
- if (hasCustomSvgMask) {
3666
+ const hasEdgeFadeMask = Boolean(currentClipPath && currentClipPath.__edgeFadeMask);
3667
+ if (hasCustomSvgMask || hasEdgeFadeMask) {
3667
3668
  if (isSvgMaskClipPath(currentClipPath)) {
3668
3669
  syncSvgMaskClipPath(g);
3669
3670
  } else if (currentClipPath && typeof currentClipPath.set === "function") {
3670
- const baseW = Math.max(1, Number(currentClipPath.width) || frameW);
3671
- const baseH = Math.max(1, Number(currentClipPath.height) || frameH);
3672
- currentClipPath.set({
3673
- left: 0,
3674
- top: 0,
3675
- originX: "center",
3676
- originY: "center",
3677
- scaleX: frameW / baseW,
3678
- scaleY: frameH / baseH,
3679
- selectable: false,
3680
- evented: false,
3681
- hasControls: false,
3682
- hasBorders: false
3683
- });
3684
- currentClipPath.absolutePositioned = false;
3685
- currentClipPath.excludeFromExport = true;
3686
- currentClipPath.dirty = true;
3687
- currentClipPath.setCoords();
3671
+ if (hasEdgeFadeMask) {
3672
+ currentClipPath.dirty = true;
3673
+ } else {
3674
+ const baseW = Math.max(1, Number(currentClipPath.width) || frameW);
3675
+ const baseH = Math.max(1, Number(currentClipPath.height) || frameH);
3676
+ currentClipPath.set({
3677
+ left: 0,
3678
+ top: 0,
3679
+ originX: "center",
3680
+ originY: "center",
3681
+ scaleX: frameW / baseW,
3682
+ scaleY: frameH / baseH,
3683
+ selectable: false,
3684
+ evented: false,
3685
+ hasControls: false,
3686
+ hasBorders: false
3687
+ });
3688
+ currentClipPath.absolutePositioned = false;
3689
+ currentClipPath.excludeFromExport = true;
3690
+ currentClipPath.dirty = true;
3691
+ currentClipPath.setCoords();
3692
+ }
3688
3693
  }
3689
3694
  } else {
3690
3695
  const needsNewClipPath = !g.clipPath || shape === "circle" && !(g.clipPath instanceof fabric.Ellipse) || shape !== "circle" && !(g.clipPath instanceof fabric.Rect);
@@ -4641,10 +4646,12 @@ function exitCropMode(g, commit = true) {
4641
4646
  g.hasControls = true;
4642
4647
  g.borderDashArray = void 0;
4643
4648
  installCanvaMaskControls(g);
4644
- g.borderColor = void 0;
4645
- g.cornerColor = void 0;
4646
- g.cornerStrokeColor = void 0;
4649
+ g.borderColor = "#4f46e5";
4650
+ g.borderDashArray = void 0;
4651
+ g.cornerColor = "#ffffff";
4652
+ g.cornerStrokeColor = "#4f46e5";
4647
4653
  g.cornerStyle = "rect";
4654
+ g.transparentCorners = false;
4648
4655
  g[CROP_MODE_FLAG] = false;
4649
4656
  g.__cropZoomLastPointer = void 0;
4650
4657
  g.__lastPointerForCrop = void 0;
@@ -6174,6 +6181,127 @@ function renderSmartElementToDataUri(type, props, width, height) {
6174
6181
  if (!svg) return null;
6175
6182
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
6176
6183
  }
6184
+ function hasEdgeFade(p) {
6185
+ if (!p) return false;
6186
+ const sides = [
6187
+ [p.fadeTopSize, p.fadeTopAmount],
6188
+ [p.fadeRightSize, p.fadeRightAmount],
6189
+ [p.fadeBottomSize, p.fadeBottomAmount],
6190
+ [p.fadeLeftSize, p.fadeLeftAmount]
6191
+ ];
6192
+ for (const [size, amount] of sides) {
6193
+ const s = Number(size) || 0;
6194
+ const a = amount == null ? 1 : Number(amount);
6195
+ if (s > 0 && a < 1) return true;
6196
+ }
6197
+ return false;
6198
+ }
6199
+ function edgeFadeKey(p) {
6200
+ if (!hasEdgeFade(p)) return "";
6201
+ return [
6202
+ (p == null ? void 0 : p.fadeTopSize) ?? 0,
6203
+ (p == null ? void 0 : p.fadeTopAmount) ?? 1,
6204
+ (p == null ? void 0 : p.fadeRightSize) ?? 0,
6205
+ (p == null ? void 0 : p.fadeRightAmount) ?? 1,
6206
+ (p == null ? void 0 : p.fadeBottomSize) ?? 0,
6207
+ (p == null ? void 0 : p.fadeBottomAmount) ?? 1,
6208
+ (p == null ? void 0 : p.fadeLeftSize) ?? 0,
6209
+ (p == null ? void 0 : p.fadeLeftAmount) ?? 1,
6210
+ (p == null ? void 0 : p.fadeTopHardness) ?? 1,
6211
+ (p == null ? void 0 : p.fadeRightHardness) ?? 1,
6212
+ (p == null ? void 0 : p.fadeBottomHardness) ?? 1,
6213
+ (p == null ? void 0 : p.fadeLeftHardness) ?? 1
6214
+ ].join("|");
6215
+ }
6216
+ function clamp01(n) {
6217
+ if (!Number.isFinite(n)) return 0;
6218
+ return Math.max(0, Math.min(1, n));
6219
+ }
6220
+ function bakeEdgeFade(source, fade) {
6221
+ const w = source.naturalWidth || source.width || 0;
6222
+ const h = source.naturalHeight || source.height || 0;
6223
+ const canvas = document.createElement("canvas");
6224
+ canvas.width = Math.max(1, w);
6225
+ canvas.height = Math.max(1, h);
6226
+ const ctx = canvas.getContext("2d");
6227
+ if (!ctx) return canvas;
6228
+ ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
6229
+ const clampHardness = (n) => {
6230
+ const v = Number(n);
6231
+ if (!Number.isFinite(v) || v <= 0) return 1;
6232
+ return Math.max(0.1, Math.min(5, v));
6233
+ };
6234
+ const sides = [
6235
+ { side: "top", size: clamp01(fade.fadeTopSize ?? 0), amount: clamp01(fade.fadeTopAmount ?? 1), hardness: clampHardness(fade.fadeTopHardness) },
6236
+ { side: "right", size: clamp01(fade.fadeRightSize ?? 0), amount: clamp01(fade.fadeRightAmount ?? 1), hardness: clampHardness(fade.fadeRightHardness) },
6237
+ { side: "bottom", size: clamp01(fade.fadeBottomSize ?? 0), amount: clamp01(fade.fadeBottomAmount ?? 1), hardness: clampHardness(fade.fadeBottomHardness) },
6238
+ { side: "left", size: clamp01(fade.fadeLeftSize ?? 0), amount: clamp01(fade.fadeLeftAmount ?? 1), hardness: clampHardness(fade.fadeLeftHardness) }
6239
+ ];
6240
+ for (const { side, size, amount, hardness } of sides) {
6241
+ if (size <= 0 || amount >= 1) continue;
6242
+ const mask = document.createElement("canvas");
6243
+ mask.width = canvas.width;
6244
+ mask.height = canvas.height;
6245
+ const mctx = mask.getContext("2d");
6246
+ if (!mctx) continue;
6247
+ mctx.fillStyle = "#000";
6248
+ mctx.fillRect(0, 0, mask.width, mask.height);
6249
+ let g;
6250
+ let x = 0, y = 0, rectW = mask.width, rectH = mask.height;
6251
+ const STOPS = 24;
6252
+ const innerAlpha = (t) => {
6253
+ const keepProgress = Math.pow(t, hardness);
6254
+ const erase = (1 - amount) * (1 - keepProgress);
6255
+ return 1 - erase;
6256
+ };
6257
+ const fillStops = (grad, reverse) => {
6258
+ for (let i = 0; i <= STOPS; i++) {
6259
+ const t = i / STOPS;
6260
+ const a = innerAlpha(t);
6261
+ grad.addColorStop(t, `rgba(0,0,0,${a})`);
6262
+ }
6263
+ };
6264
+ if (side === "top") {
6265
+ const band = Math.max(1, Math.round(mask.height * size));
6266
+ g = mctx.createLinearGradient(0, 0, 0, band);
6267
+ fillStops(g);
6268
+ rectH = band;
6269
+ } else if (side === "bottom") {
6270
+ const band = Math.max(1, Math.round(mask.height * size));
6271
+ y = mask.height - band;
6272
+ g = mctx.createLinearGradient(0, mask.height, 0, y);
6273
+ fillStops(g);
6274
+ rectH = band;
6275
+ } else if (side === "left") {
6276
+ const band = Math.max(1, Math.round(mask.width * size));
6277
+ g = mctx.createLinearGradient(0, 0, band, 0);
6278
+ fillStops(g);
6279
+ rectW = band;
6280
+ } else {
6281
+ const band = Math.max(1, Math.round(mask.width * size));
6282
+ x = mask.width - band;
6283
+ g = mctx.createLinearGradient(mask.width, 0, x, 0);
6284
+ fillStops(g);
6285
+ rectW = band;
6286
+ }
6287
+ mctx.fillStyle = g;
6288
+ mctx.fillRect(x, y, rectW, rectH);
6289
+ if (amount <= 1e-3) {
6290
+ const edgePx = Math.min(2, side === "top" || side === "bottom" ? rectH : rectW);
6291
+ mctx.fillStyle = "rgba(0,0,0,0)";
6292
+ mctx.globalCompositeOperation = "copy";
6293
+ if (side === "top") mctx.fillRect(0, 0, mask.width, edgePx);
6294
+ if (side === "bottom") mctx.fillRect(0, mask.height - edgePx, mask.width, edgePx);
6295
+ if (side === "left") mctx.fillRect(0, 0, edgePx, mask.height);
6296
+ if (side === "right") mctx.fillRect(mask.width - edgePx, 0, edgePx, mask.height);
6297
+ mctx.globalCompositeOperation = "source-over";
6298
+ }
6299
+ ctx.globalCompositeOperation = "destination-in";
6300
+ ctx.drawImage(mask, 0, 0);
6301
+ ctx.globalCompositeOperation = "source-over";
6302
+ }
6303
+ return canvas;
6304
+ }
6177
6305
  function angleToCoords(angleDeg) {
6178
6306
  const rad = angleDeg * Math.PI / 180;
6179
6307
  const x1 = 0.5 - Math.sin(rad) * 0.5;
@@ -7683,6 +7811,10 @@ const PageCanvas = forwardRef(
7683
7811
  fabricCanvas.on("mouse:dblclick", (e) => {
7684
7812
  if (!isActiveRef.current || !allowEditing) return;
7685
7813
  let target = e.target;
7814
+ if (!target) {
7815
+ const active = fabricCanvas.getActiveObject();
7816
+ if (active) target = active;
7817
+ }
7686
7818
  if (target && target instanceof fabric.Group && target.__cropGroup) {
7687
7819
  const ct = target.__cropData;
7688
7820
  if ((ct == null ? void 0 : ct._img) && !isCropGroupInCropMode(target)) {
@@ -7838,7 +7970,7 @@ const PageCanvas = forwardRef(
7838
7970
  visibilityUpdateInProgressRef.current = false;
7839
7971
  }
7840
7972
  doSyncRef.current = () => {
7841
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
7973
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
7842
7974
  const shouldSkipUpdates2 = syncLockedRef.current || editLockRef.current;
7843
7975
  const state = useEditorStore.getState();
7844
7976
  const elementsToSync = elements;
@@ -7999,15 +8131,22 @@ const PageCanvas = forwardRef(
7999
8131
  const storedImageUrl = existingObj.__imageSrc;
8000
8132
  const currentUrlNormalized = currentImageUrl.trim();
8001
8133
  const storedUrlNormalized = storedImageUrl ? String(storedImageUrl).trim() : "";
8002
- const svgColorMapStr = element.svgColorMap ? JSON.stringify(element.svgColorMap) : "";
8003
- const storedColorMapStr = existingObj.__svgColorMap || "";
8004
- const colorMapChanged = svgColorMapStr !== storedColorMapStr;
8005
- const sourceUrlChanged = currentUrlNormalized !== storedUrlNormalized;
8006
- const needsReload = sourceUrlChanged || colorMapChanged;
8007
8134
  const hasUrl = currentUrlNormalized !== "";
8008
8135
  const isCropGroup2 = existingObj instanceof fabric.Group && existingObj.__cropGroup;
8009
8136
  const isPlaceholderGroup = isEmptyImagePlaceholderGroup(existingObj);
8010
8137
  const isPlaceholder = isPlaceholderGroup || !(existingObj instanceof fabric.FabricImage) || existingObj instanceof fabric.Group && !isCropGroup2;
8138
+ const svgColorMapStr = element.svgColorMap ? JSON.stringify(element.svgColorMap) : "";
8139
+ const storedColorMapStr = existingObj.__svgColorMap || "";
8140
+ const colorMapChanged = svgColorMapStr !== storedColorMapStr;
8141
+ const sourceUrlChanged = currentUrlNormalized !== storedUrlNormalized;
8142
+ const newFadeKey = edgeFadeKey(element);
8143
+ const oldFadeKey = isCropGroup2 ? existingObj.__edgeFadeInputKey || "" : existingObj.__edgeFadeKey || "";
8144
+ const innerImg = (_e = existingObj == null ? void 0 : existingObj.__cropData) == null ? void 0 : _e._img;
8145
+ const innerOldKey = isCropGroup2 ? oldFadeKey : innerImg ? innerImg.__edgeFadeKey || "" : oldFadeKey;
8146
+ const cropFadeRendererMissing = isCropGroup2 && Boolean(newFadeKey) && !existingObj.__edgeFadeRenderConfig;
8147
+ const fadeKeyChanged = newFadeKey !== oldFadeKey || newFadeKey !== innerOldKey || cropFadeRendererMissing;
8148
+ const needsReload = sourceUrlChanged || colorMapChanged || fadeKeyChanged && !isCropGroup2;
8149
+ const needsCropGroupFadeUpdate = isCropGroup2 && fadeKeyChanged;
8011
8150
  const hadUrlBefore = storedImageUrl && String(storedImageUrl).trim() !== "";
8012
8151
  if (!hasUrl && hadUrlBefore) {
8013
8152
  const placeholder = isCropGroup2 ? createImagePlaceholderForGroup(element) : createImagePlaceholder(element);
@@ -8023,16 +8162,16 @@ const PageCanvas = forwardRef(
8023
8162
  fc.requestRenderAll();
8024
8163
  continue;
8025
8164
  }
8026
- const imageFitForReplace = element.imageFit || ((_e = element.style) == null ? void 0 : _e.imageFit) || "cover";
8027
- const clipShapeForReplace = element.clipShape ?? ((_f = element.style) == null ? void 0 : _f.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
8165
+ const imageFitForReplace = element.imageFit || ((_f = element.style) == null ? void 0 : _f.imageFit) || "cover";
8166
+ const clipShapeForReplace = element.clipShape ?? ((_g = element.style) == null ? void 0 : _g.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
8028
8167
  const needCropGroupForElement = imageFitForReplace !== "fill" || clipShapeForReplace && clipShapeForReplace !== "none";
8029
8168
  const plainImageNeedsCropGroup = hasUrl && !isCropGroup2 && existingObj instanceof fabric.FabricImage && needCropGroupForElement;
8030
- if (hasUrl && (needsReload || isPlaceholder || plainImageNeedsCropGroup)) {
8169
+ if (hasUrl && (needsReload || isPlaceholder || plainImageNeedsCropGroup || needsCropGroupFadeUpdate)) {
8031
8170
  if (needsReload && !isBeingTransformed && (!wasJustModified || sourceUrlChanged)) {
8032
8171
  loadImageAsync(element, existingObj, fc);
8033
8172
  } else if (plainImageNeedsCropGroup) {
8034
8173
  loadImageAsync(element, existingObj, fc);
8035
- } else if (!needsReload && isCropGroup2) {
8174
+ } else if ((!needsReload || needsCropGroupFadeUpdate) && isCropGroup2) {
8036
8175
  const ct = existingObj.__cropData;
8037
8176
  if (ct) {
8038
8177
  const resolvedCrop = pageTree.length > 0 ? getNodeBounds(element, pageTree) : { width: typeof element.width === "number" ? element.width : 200, height: typeof element.height === "number" ? element.height : 50 };
@@ -8129,6 +8268,7 @@ const PageCanvas = forwardRef(
8129
8268
  }
8130
8269
  }
8131
8270
  updateCoverLayout(existingObj);
8271
+ applyEdgeFadeFrameClipPath(existingObj, element, ct.frameW, ct.frameH, ct.shape || "rect", ct.rx || 0);
8132
8272
  if (allowEditing) {
8133
8273
  installCanvaMaskControls(existingObj);
8134
8274
  } else {
@@ -8438,7 +8578,7 @@ const PageCanvas = forwardRef(
8438
8578
  fc.add(placeholder);
8439
8579
  fc.bringObjectToFront(placeholder);
8440
8580
  const activeObj = fc.getActiveObject();
8441
- if (activeObj && (((_g = activeObj._ct) == null ? void 0 : _g.isCropGroup) || activeObj.__cropGroup)) {
8581
+ if (activeObj && (((_h = activeObj._ct) == null ? void 0 : _h.isCropGroup) || activeObj.__cropGroup)) {
8442
8582
  fc.setActiveObject(activeObj);
8443
8583
  }
8444
8584
  placeholder.dirty = true;
@@ -8478,7 +8618,7 @@ const PageCanvas = forwardRef(
8478
8618
  fc.add(obj);
8479
8619
  fc.bringObjectToFront(obj);
8480
8620
  const activeObj = fc.getActiveObject();
8481
- if (activeObj && (((_h = activeObj._ct) == null ? void 0 : _h.isCropGroup) || activeObj.__cropGroup)) {
8621
+ if (activeObj && (((_i = activeObj._ct) == null ? void 0 : _i.isCropGroup) || activeObj.__cropGroup)) {
8482
8622
  fc.setActiveObject(activeObj);
8483
8623
  }
8484
8624
  obj.dirty = true;
@@ -8512,7 +8652,7 @@ const PageCanvas = forwardRef(
8512
8652
  isRebuildingRef.current = false;
8513
8653
  fc.requestRenderAll();
8514
8654
  if (activeBeforeSync && fc.getObjects().includes(activeBeforeSync)) {
8515
- const isCropGroup2 = ((_i = activeBeforeSync._ct) == null ? void 0 : _i.isCropGroup) || activeBeforeSync.__cropGroup;
8655
+ const isCropGroup2 = ((_j = activeBeforeSync._ct) == null ? void 0 : _j.isCropGroup) || activeBeforeSync.__cropGroup;
8516
8656
  if (isCropGroup2) {
8517
8657
  fc.setActiveObject(activeBeforeSync);
8518
8658
  fc.requestRenderAll();
@@ -8934,6 +9074,7 @@ const PageCanvas = forwardRef(
8934
9074
  }
8935
9075
  }
8936
9076
  updateCoverLayout(obj);
9077
+ applyEdgeFadeFrameClipPath(obj, element, elementWidth, elementHeight, ct.shape || "rect", ct.rx || 0);
8937
9078
  obj.setCoords();
8938
9079
  obj.dirty = true;
8939
9080
  if (obj.clipPath) {
@@ -9517,8 +9658,131 @@ const PageCanvas = forwardRef(
9517
9658
  obj.__lastStrokeGradientJson = "null";
9518
9659
  }
9519
9660
  };
9661
+ const clamp2 = (v, min, max) => {
9662
+ return Math.max(min, Math.min(max, v));
9663
+ };
9664
+ const applyEdgeFadeFrameClipPath = (group, element, frameW, frameH, shape, rxRatio) => {
9665
+ var _a, _b, _c;
9666
+ const fadeElement = element;
9667
+ const fadeGroup = group;
9668
+ const inputKey = edgeFadeKey(fadeElement);
9669
+ const hasFade = hasEdgeFade(fadeElement);
9670
+ if (!fadeGroup.__edgeFadeOriginalDrawObject) {
9671
+ fadeGroup.__edgeFadeOriginalDrawObject = group.drawObject;
9672
+ }
9673
+ if (hasFade) {
9674
+ if ((_a = group.clipPath) == null ? void 0 : _a.__edgeFadeMask) {
9675
+ group.clipPath = void 0;
9676
+ updateCoverLayout(group);
9677
+ }
9678
+ fadeGroup.__edgeFadeRenderConfig = {
9679
+ key: `${inputKey}|${Math.round(frameW)}|${Math.round(frameH)}|${shape}|${rxRatio}`,
9680
+ frameW: Math.max(1, Number(frameW) || 1),
9681
+ frameH: Math.max(1, Number(frameH) || 1),
9682
+ topSize: clamp2(Number(fadeElement.fadeTopSize) || 0, 0, 1),
9683
+ topAmount: clamp2(fadeElement.fadeTopAmount == null ? 1 : Number(fadeElement.fadeTopAmount), 0, 1),
9684
+ rightSize: clamp2(Number(fadeElement.fadeRightSize) || 0, 0, 1),
9685
+ rightAmount: clamp2(fadeElement.fadeRightAmount == null ? 1 : Number(fadeElement.fadeRightAmount), 0, 1),
9686
+ bottomSize: clamp2(Number(fadeElement.fadeBottomSize) || 0, 0, 1),
9687
+ bottomAmount: clamp2(fadeElement.fadeBottomAmount == null ? 1 : Number(fadeElement.fadeBottomAmount), 0, 1),
9688
+ leftSize: clamp2(Number(fadeElement.fadeLeftSize) || 0, 0, 1),
9689
+ leftAmount: clamp2(fadeElement.fadeLeftAmount == null ? 1 : Number(fadeElement.fadeLeftAmount), 0, 1),
9690
+ topHardness: clamp2(Number(fadeElement.fadeTopHardness) || 1, 0.1, 5),
9691
+ rightHardness: clamp2(Number(fadeElement.fadeRightHardness) || 1, 0.1, 5),
9692
+ bottomHardness: clamp2(Number(fadeElement.fadeBottomHardness) || 1, 0.1, 5),
9693
+ leftHardness: clamp2(Number(fadeElement.fadeLeftHardness) || 1, 0.1, 5)
9694
+ };
9695
+ const originalDrawObject = fadeGroup.__edgeFadeOriginalDrawObject ?? group.drawObject;
9696
+ group.drawObject = function edgeFadeDrawObject(ctx, forClipping, context) {
9697
+ originalDrawObject.call(this, ctx, forClipping, context);
9698
+ if (forClipping) return;
9699
+ const cfg = this.__edgeFadeRenderConfig;
9700
+ if (!cfg) return;
9701
+ const liveW = Number(this.width) || 0;
9702
+ const liveH = Number(this.height) || 0;
9703
+ const w = liveW > 0 ? liveW : cfg.frameW;
9704
+ const h = liveH > 0 ? liveH : cfg.frameH;
9705
+ const x = -w / 2;
9706
+ const y = -h / 2;
9707
+ const paintBand = (side, size, amount, hardness) => {
9708
+ if (size <= 0 || amount >= 1) return;
9709
+ ctx.save();
9710
+ ctx.globalCompositeOperation = "destination-out";
9711
+ let gradient;
9712
+ const eraseAtEdge = 1 - amount;
9713
+ const STOPS = 24;
9714
+ const addStops = (g) => {
9715
+ for (let i = 0; i <= STOPS; i++) {
9716
+ const t = i / STOPS;
9717
+ const keepProgress = Math.pow(t, hardness);
9718
+ const a = eraseAtEdge * (1 - keepProgress);
9719
+ g.addColorStop(t, `rgba(0,0,0,${a})`);
9720
+ }
9721
+ };
9722
+ if (side === "top") {
9723
+ const band = Math.max(1, h * size);
9724
+ gradient = ctx.createLinearGradient(0, y, 0, y + band);
9725
+ addStops(gradient);
9726
+ ctx.fillStyle = gradient;
9727
+ ctx.fillRect(x, y, w, band);
9728
+ if (amount <= 1e-3) ctx.fillRect(x, y, w, Math.min(2, band));
9729
+ } else if (side === "bottom") {
9730
+ const band = Math.max(1, h * size);
9731
+ gradient = ctx.createLinearGradient(0, y + h, 0, y + h - band);
9732
+ addStops(gradient);
9733
+ ctx.fillStyle = gradient;
9734
+ ctx.fillRect(x, y + h - band, w, band);
9735
+ if (amount <= 1e-3) ctx.fillRect(x, y + h - Math.min(2, band), w, Math.min(2, band));
9736
+ } else if (side === "left") {
9737
+ const band = Math.max(1, w * size);
9738
+ gradient = ctx.createLinearGradient(x, 0, x + band, 0);
9739
+ addStops(gradient);
9740
+ ctx.fillStyle = gradient;
9741
+ ctx.fillRect(x, y, band, h);
9742
+ if (amount <= 1e-3) ctx.fillRect(x, y, Math.min(2, band), h);
9743
+ } else {
9744
+ const band = Math.max(1, w * size);
9745
+ gradient = ctx.createLinearGradient(x + w, 0, x + w - band, 0);
9746
+ addStops(gradient);
9747
+ ctx.fillStyle = gradient;
9748
+ ctx.fillRect(x + w - band, y, band, h);
9749
+ if (amount <= 1e-3) ctx.fillRect(x + w - Math.min(2, band), y, Math.min(2, band), h);
9750
+ }
9751
+ ctx.restore();
9752
+ };
9753
+ paintBand("top", cfg.topSize, cfg.topAmount, cfg.topHardness);
9754
+ paintBand("right", cfg.rightSize, cfg.rightAmount, cfg.rightHardness);
9755
+ paintBand("bottom", cfg.bottomSize, cfg.bottomAmount, cfg.bottomHardness);
9756
+ paintBand("left", cfg.leftSize, cfg.leftAmount, cfg.leftHardness);
9757
+ };
9758
+ group.set({ objectCaching: true, noScaleCache: false });
9759
+ fadeGroup.__edgeFadeKey = fadeGroup.__edgeFadeRenderConfig.key;
9760
+ fadeGroup.__edgeFadeInputKey = inputKey;
9761
+ } else {
9762
+ if (fadeGroup.__edgeFadeOriginalDrawObject) {
9763
+ group.drawObject = fadeGroup.__edgeFadeOriginalDrawObject;
9764
+ }
9765
+ delete fadeGroup.__edgeFadeRenderConfig;
9766
+ delete fadeGroup.__edgeFadeKey;
9767
+ delete fadeGroup.__edgeFadeInputKey;
9768
+ if ((_b = group.clipPath) == null ? void 0 : _b.__edgeFadeMask) {
9769
+ group.clipPath = void 0;
9770
+ updateCoverLayout(group);
9771
+ }
9772
+ }
9773
+ if (group.clipPath) {
9774
+ group.clipPath.dirty = true;
9775
+ group.clipPath.setCoords();
9776
+ }
9777
+ fadeGroup.dirty = true;
9778
+ const children = group.getObjects();
9779
+ children.forEach((child) => {
9780
+ child.dirty = true;
9781
+ });
9782
+ (_c = group.canvas) == null ? void 0 : _c.requestRenderAll();
9783
+ };
9520
9784
  const loadImageAsync = async (element, placeholder, fc) => {
9521
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
9785
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
9522
9786
  const imageUrl = element.src || element.imageUrl;
9523
9787
  if (!imageUrl) return;
9524
9788
  const elementId = element.id;
@@ -9531,7 +9795,9 @@ const PageCanvas = forwardRef(
9531
9795
  const existingImg = ct == null ? void 0 : ct._img;
9532
9796
  const existingSrc = placeholder.__imageSrc;
9533
9797
  const existingSvgColorMap = placeholder.__svgColorMap || "";
9534
- if (existingImg && existingSrc === imageUrl && existingSvgColorMap === nextSvgColorMap) {
9798
+ const existingFadeKey = existingImg && existingImg.__edgeFadeKey || placeholder.__edgeFadeKey || "";
9799
+ const nextFadeKey = edgeFadeKey(element);
9800
+ if (existingImg && existingSrc === imageUrl && existingSvgColorMap === nextSvgColorMap && existingFadeKey === nextFadeKey) {
9535
9801
  return placeholder;
9536
9802
  }
9537
9803
  }
@@ -9548,6 +9814,22 @@ const PageCanvas = forwardRef(
9548
9814
  if (!fabricRef.current || !isLatestRequest()) return;
9549
9815
  await normalizeSvgImageDimensions(img, imageUrl, element.sourceFormat);
9550
9816
  if (!isLatestRequest()) return;
9817
+ const imageFitForFade = element.imageFit || ((_a = element.style) == null ? void 0 : _a.imageFit) || "cover";
9818
+ const clipShapeForFade = element.clipShape ?? ((_b = element.style) == null ? void 0 : _b.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
9819
+ const willUseCropGroupForFade = imageFitForFade !== "fill" || clipShapeForFade && clipShapeForFade !== "none";
9820
+ try {
9821
+ if (hasEdgeFade(element) && !willUseCropGroupForFade) {
9822
+ const srcEl = (_c = img.getElement) == null ? void 0 : _c.call(img);
9823
+ if (srcEl) {
9824
+ const baked = bakeEdgeFade(srcEl, element);
9825
+ img.setElement(baked);
9826
+ img.__edgeFadeKey = edgeFadeKey(element);
9827
+ img.dirty = true;
9828
+ }
9829
+ }
9830
+ } catch (e) {
9831
+ console.warn("[edgeFade] bake failed:", e);
9832
+ }
9551
9833
  const isHidden = !element.visible;
9552
9834
  img.set({
9553
9835
  originX: "left",
@@ -9580,8 +9862,8 @@ const PageCanvas = forwardRef(
9580
9862
  });
9581
9863
  img.setCoords();
9582
9864
  } else {
9583
- const imageFit = element.imageFit || ((_a = element.style) == null ? void 0 : _a.imageFit) || "cover";
9584
- const clipShape = element.clipShape ?? ((_b = element.style) == null ? void 0 : _b.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
9865
+ const imageFit = element.imageFit || ((_d = element.style) == null ? void 0 : _d.imageFit) || "cover";
9866
+ const clipShape = element.clipShape ?? ((_e = element.style) == null ? void 0 : _e.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
9585
9867
  const needCropGroup2 = imageFit !== "fill" || clipShape && clipShape !== "none";
9586
9868
  const imgNaturalWidth = img.width || 1;
9587
9869
  const imgNaturalHeight = img.height || 1;
@@ -9608,7 +9890,7 @@ const PageCanvas = forwardRef(
9608
9890
  if (imageFit === "fill" && !needCropGroup2) {
9609
9891
  const finalScaleX = baseScaleX * (element.scaleX ?? 1);
9610
9892
  const finalScaleY = baseScaleY * (element.scaleY ?? 1);
9611
- const pageTreeForCreate = ((pageChildren == null ? void 0 : pageChildren.length) ? pageChildren : (_c = useEditorStore.getState().canvas.pages.find((p) => p.id === pageId)) == null ? void 0 : _c.children) ?? [];
9893
+ const pageTreeForCreate = ((pageChildren == null ? void 0 : pageChildren.length) ? pageChildren : (_f = useEditorStore.getState().canvas.pages.find((p) => p.id === pageId)) == null ? void 0 : _f.children) ?? [];
9612
9894
  const createPos = pageTreeForCreate.length > 0 ? (() => {
9613
9895
  const node = findNodeById(pageTreeForCreate, element.id);
9614
9896
  return node ? getAbsoluteBounds(node, pageTreeForCreate) : { left: element.left ?? 0, top: element.top ?? 0 };
@@ -9653,12 +9935,12 @@ const PageCanvas = forwardRef(
9653
9935
  }
9654
9936
  img.__imageSrc = imageUrl;
9655
9937
  img.__svgColorMap = element.svgColorMap ? JSON.stringify(element.svgColorMap) : "";
9656
- const imageFitFinal = element.imageFit || ((_d = element.style) == null ? void 0 : _d.imageFit) || "cover";
9657
- const clipShapeFinal = element.clipShape ?? ((_e = element.style) == null ? void 0 : _e.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
9938
+ const imageFitFinal = element.imageFit || ((_g = element.style) == null ? void 0 : _g.imageFit) || "cover";
9939
+ const clipShapeFinal = element.clipShape ?? ((_h = element.style) == null ? void 0 : _h.imageFrameShape) ?? (isPreviewMode ? "rectangle" : "none");
9658
9940
  const needCropGroup = imageFitFinal !== "fill" || clipShapeFinal && clipShapeFinal !== "none";
9659
9941
  let finalObject = img;
9660
9942
  if (needCropGroup) {
9661
- const pageTreeForCropResolve = ((pageChildren == null ? void 0 : pageChildren.length) ? pageChildren : (_f = useEditorStore.getState().canvas.pages.find((p) => p.id === pageId)) == null ? void 0 : _f.children) ?? [];
9943
+ const pageTreeForCropResolve = ((pageChildren == null ? void 0 : pageChildren.length) ? pageChildren : (_i = useEditorStore.getState().canvas.pages.find((p) => p.id === pageId)) == null ? void 0 : _i.children) ?? [];
9662
9944
  const nodeForSize = pageTreeForCropResolve.length ? findNodeById(pageTreeForCropResolve, element.id) : null;
9663
9945
  const w = nodeForSize && isElement(nodeForSize) ? nodeForSize.width : element.width;
9664
9946
  const h = nodeForSize && isElement(nodeForSize) ? nodeForSize.height : element.height;
@@ -9690,16 +9972,16 @@ const PageCanvas = forwardRef(
9690
9972
  let panY = element.cropPanY ?? 0.5;
9691
9973
  let zoom2 = element.cropZoom ?? 1;
9692
9974
  if (existingCropGroup) {
9693
- const existingImg = (_g = existingCropGroup.__cropData) == null ? void 0 : _g._img;
9975
+ const existingImg = (_j = existingCropGroup.__cropData) == null ? void 0 : _j._img;
9694
9976
  if (existingImg) {
9695
- panX = ((_h = existingImg._ct) == null ? void 0 : _h.panX) ?? existingImg.__panX ?? panX;
9696
- panY = ((_i = existingImg._ct) == null ? void 0 : _i.panY) ?? existingImg.__panY ?? panY;
9697
- zoom2 = ((_j = existingImg._ct) == null ? void 0 : _j.zoom) ?? zoom2;
9977
+ panX = ((_k = existingImg._ct) == null ? void 0 : _k.panX) ?? existingImg.__panX ?? panX;
9978
+ panY = ((_l = existingImg._ct) == null ? void 0 : _l.panY) ?? existingImg.__panY ?? panY;
9979
+ zoom2 = ((_m = existingImg._ct) == null ? void 0 : _m.zoom) ?? zoom2;
9698
9980
  }
9699
9981
  }
9700
9982
  const isDynamicField = dynamicFieldIds.includes(element.id);
9701
9983
  const canBeEvented = isEditorMode || isPreviewMode && isDynamicField;
9702
- const pageTreeForCrop = ((pageChildren == null ? void 0 : pageChildren.length) ? pageChildren : (_k = useEditorStore.getState().canvas.pages.find((p) => p.id === pageId)) == null ? void 0 : _k.children) ?? [];
9984
+ const pageTreeForCrop = ((pageChildren == null ? void 0 : pageChildren.length) ? pageChildren : (_n = useEditorStore.getState().canvas.pages.find((p) => p.id === pageId)) == null ? void 0 : _n.children) ?? [];
9703
9985
  const createPosForCrop = pageTreeForCrop.length > 0 ? (() => {
9704
9986
  const node = findNodeById(pageTreeForCrop, element.id);
9705
9987
  return node ? getAbsoluteBounds(node, pageTreeForCrop) : { left: element.left ?? 0, top: element.top ?? 0 };
@@ -9750,17 +10032,18 @@ const PageCanvas = forwardRef(
9750
10032
  } else {
9751
10033
  installCanvaMaskControls(cropGroup);
9752
10034
  }
9753
- const cropImg = (_l = cropGroup.__cropData) == null ? void 0 : _l._img;
10035
+ const cropImg = (_o = cropGroup.__cropData) == null ? void 0 : _o._img;
9754
10036
  if (cropImg) {
9755
10037
  cropImg._ct = { panX, panY, zoom: zoom2 };
9756
10038
  updateCoverLayout(cropGroup);
9757
10039
  }
10040
+ applyEdgeFadeFrameClipPath(cropGroup, element, frameW, frameH, shape, rxRatio);
9758
10041
  setObjectData(cropGroup, element.id);
9759
10042
  cropGroup.__imageElement = cropImg;
9760
10043
  cropGroup.__imageSrc = imageUrl;
9761
10044
  cropGroup.__svgColorMap = nextSvgColorMap;
9762
10045
  if (cropImg && element.imageNaturalWidth == null) {
9763
- const el = ((_m = cropImg.getElement) == null ? void 0 : _m.call(cropImg)) ?? cropImg._element;
10046
+ const el = ((_p = cropImg.getElement) == null ? void 0 : _p.call(cropImg)) ?? cropImg._element;
9764
10047
  const orig = typeof cropImg.getOriginalSize === "function" ? cropImg.getOriginalSize() : null;
9765
10048
  const nw = (orig == null ? void 0 : orig.width) ?? (el == null ? void 0 : el.naturalWidth) ?? cropImg.width;
9766
10049
  const nh = (orig == null ? void 0 : orig.height) ?? (el == null ? void 0 : el.naturalHeight) ?? cropImg.height;
@@ -13921,12 +14204,69 @@ class PixldocsRenderer {
13921
14204
  { width: canvasWidth, height: canvasHeight },
13922
14205
  { cssOnly: false, backstoreOnly: false }
13923
14206
  );
14207
+ const fadeBakeRecords = [];
14208
+ try {
14209
+ const objs = fabricInstance.getObjects().slice();
14210
+ for (const obj of objs) {
14211
+ const isFadedCropGroup = obj instanceof fabric.Group && (Boolean(obj.__edgeFadeRenderConfig) || Boolean(obj.__edgeFadeKey) || Boolean(obj.__edgeFadeInputKey));
14212
+ if (!isFadedCropGroup) continue;
14213
+ try {
14214
+ const baked = obj.toCanvasElement({
14215
+ multiplier: 2,
14216
+ enableRetinaScaling: false
14217
+ });
14218
+ const rect = obj.getBoundingRect();
14219
+ const replacement = new fabric.FabricImage(baked, {
14220
+ left: rect.left,
14221
+ top: rect.top,
14222
+ originX: "left",
14223
+ originY: "top",
14224
+ scaleX: rect.width / baked.width,
14225
+ scaleY: rect.height / baked.height,
14226
+ selectable: false,
14227
+ evented: false,
14228
+ objectCaching: false
14229
+ });
14230
+ const insertIndex = fabricInstance._objects.indexOf(obj);
14231
+ const prevExclude = obj.excludeFromExport;
14232
+ obj.excludeFromExport = true;
14233
+ if (insertIndex >= 0) {
14234
+ fabricInstance.insertAt(insertIndex + 1, replacement);
14235
+ } else {
14236
+ fabricInstance.add(replacement);
14237
+ }
14238
+ fadeBakeRecords.push({
14239
+ original: obj,
14240
+ replacement,
14241
+ prevExclude,
14242
+ insertIndex
14243
+ });
14244
+ } catch (bakeErr) {
14245
+ console.warn("[canvas-renderer][edgeFade] bake-for-svg failed:", bakeErr);
14246
+ }
14247
+ }
14248
+ if (fadeBakeRecords.length) {
14249
+ fabricInstance.renderAll();
14250
+ console.log(
14251
+ `[canvas-renderer][edgeFade] baked ${fadeBakeRecords.length} faded object(s) for SVG capture`
14252
+ );
14253
+ }
14254
+ } catch (e) {
14255
+ console.warn("[canvas-renderer][edgeFade] bake pass failed:", e);
14256
+ }
13924
14257
  const rawSvgString = fabricInstance.toSVG();
13925
14258
  const svgString = this.normalizeSvgDimensions(
13926
14259
  rawSvgString,
13927
14260
  canvasWidth,
13928
14261
  canvasHeight
13929
14262
  );
14263
+ for (const rec of fadeBakeRecords) {
14264
+ try {
14265
+ fabricInstance.remove(rec.replacement);
14266
+ rec.original.excludeFromExport = rec.prevExclude;
14267
+ } catch {
14268
+ }
14269
+ }
13930
14270
  fabricInstance.enableRetinaScaling = prevRetina;
13931
14271
  fabricInstance.setDimensions(
13932
14272
  { width: prevWidth, height: prevHeight },