diff --git a/src/pages/editor/components/EditorCanvas.tsx b/src/pages/editor/components/EditorCanvas.tsx
index 5925bc3..0793631 100644
--- a/src/pages/editor/components/EditorCanvas.tsx
+++ b/src/pages/editor/components/EditorCanvas.tsx
@@ -14,6 +14,7 @@ import { useCustomShapeDrawing } from "./canvas/useCustomShapeDrawing";
import { usePencilDrawing } from "./canvas/usePencilDrawing";
import ObjectsLayer from "./canvas/ObjectsLayer";
import GuidesLayer from "./canvas/GuidesLayer";
+import BlurBackdropLayer from "./canvas/BlurBackdropLayer";
import CustomShapeDrawingLayer from "./canvas/CustomShapeDrawingLayer";
import PencilDrawingLayer from "./canvas/PencilDrawingLayer";
import {
@@ -627,6 +628,13 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
+
diff --git a/src/pages/editor/components/canvas/BlurBackdropLayer.tsx b/src/pages/editor/components/canvas/BlurBackdropLayer.tsx
new file mode 100644
index 0000000..0dbdac2
--- /dev/null
+++ b/src/pages/editor/components/canvas/BlurBackdropLayer.tsx
@@ -0,0 +1,195 @@
+import { useEffect, useMemo, useState, type CSSProperties } from "react";
+import type Konva from "konva";
+import type { EditorObject } from "../../store/editorStore";
+import { getColorWithOpacity } from "../../utils/colorOpacity";
+import { toCssGradientAngle, type LinearGradient } from "../../utils/gradient";
+
+type BlurBackdropLayerProps = {
+ objects: EditorObject[];
+ stageWidth: number;
+ stageHeight: number;
+ /** Stage scale applied to Konva (editor internal scale + zoom). */
+ scale: number;
+ stageRef: React.RefObject;
+};
+
+const TRIANGLE_TOP_Y_PERCENT = 6.69873; // (1 - sqrt(3)/2) / 2
+const TRIANGLE_BOTTOM_Y_PERCENT = 93.30127; // 100 - TRIANGLE_TOP_Y_PERCENT
+
+const BlurBackdropLayer = ({ objects, stageWidth, stageHeight, scale, stageRef }: BlurBackdropLayerProps) => {
+ const [dragRevision, setDragRevision] = useState(0);
+
+ useEffect(() => {
+ const stage = stageRef.current;
+ if (!stage) return;
+
+ let raf = 0;
+ const bump = () => setDragRevision((v) => v + 1);
+
+ const scheduleBump = () => {
+ if (raf) return;
+ raf = window.requestAnimationFrame(() => {
+ raf = 0;
+ bump();
+ });
+ };
+
+ const onDragMove = () => scheduleBump();
+ const onDragEnd = () => scheduleBump();
+
+ stage.on("dragmove", onDragMove);
+ stage.on("dragend", onDragEnd);
+
+ return () => {
+ stage.off("dragmove", onDragMove);
+ stage.off("dragend", onDragEnd);
+ if (raf) window.cancelAnimationFrame(raf);
+ };
+ }, [stageRef]);
+
+ const overlays = useMemo(() => {
+ const stage = stageRef.current;
+
+ return objects
+ .map((obj, index) => {
+ if (obj.visible === false) return null;
+ if (obj.isMask) return null;
+ if (obj.type !== "rectangle") return null;
+
+ const blur = obj.blur ?? 0;
+ if (blur <= 0) return null;
+
+ const blurPx = blur * scale;
+ const rotation = obj.rotation ?? 0;
+ const shapeType = obj.shapeType ?? "square";
+
+ // Konva shapes are positioned differently depending on shapeType.
+ let leftPx = 0;
+ let topPx = 0;
+ let widthPx = (obj.width ?? 100) * scale;
+ let heightPx = (obj.height ?? 100) * scale;
+ let borderRadiusPx = 0;
+ let clipPath: string | undefined;
+ let transformOrigin: CSSProperties["transformOrigin"] = "top left";
+
+ const node = stage?.findOne(`#${obj.id}`);
+ const absPos = node && "getAbsolutePosition" in node ? node.getAbsolutePosition() : null;
+ const nodeX = absPos?.x ?? node?.x?.() ?? obj.x ?? 0;
+ const nodeY = absPos?.y ?? node?.y?.() ?? obj.y ?? 0;
+
+ if (shapeType === "circle") {
+ const sizePx = (obj.width ?? 100) * scale;
+ const radiusPx = sizePx / 2;
+ // `getAbsolutePosition()` is already in the same pixel space as our overlay container.
+ // Konva.Circle uses x/y as CENTER coordinates, while our overlay uses top-left.
+ leftPx = nodeX - radiusPx;
+ topPx = nodeY - radiusPx;
+ widthPx = sizePx;
+ heightPx = sizePx;
+ borderRadiusPx = radiusPx;
+ transformOrigin = "center center";
+ } else if (shapeType === "triangle") {
+ const baseWidth = obj.width ?? 100;
+ const baseHeight = obj.height ?? 100;
+ const side = Math.min(baseWidth, baseHeight);
+ const radius = side / 2;
+ const radiusPx = radius * scale;
+ // In Konva TriangleShape, x/y are center coordinates.
+ leftPx = nodeX - radiusPx;
+ topPx = nodeY - radiusPx;
+ widthPx = side * scale;
+ heightPx = side * scale;
+ clipPath = `polygon(50% ${TRIANGLE_TOP_Y_PERCENT}%, 0% ${TRIANGLE_BOTTOM_Y_PERCENT}%, 100% ${TRIANGLE_BOTTOM_Y_PERCENT}%)`;
+ transformOrigin = "center center";
+ } else if (shapeType === "abstract") {
+ const rawW = obj.width ?? 100;
+ const rawH = obj.height ?? 100;
+ const width = rawW * scale;
+ const height = rawH * scale;
+
+ const baseRadius = Math.min(width, height) / 2;
+ const abstractScale = 0.85;
+ const radius = baseRadius * abstractScale;
+
+ // In Konva AbstractShape, x/y are center coordinates.
+ const centerX = nodeX;
+ const centerY = nodeY;
+ leftPx = centerX - radius;
+ topPx = centerY - radius;
+ widthPx = radius * 2;
+ heightPx = radius * 2;
+ clipPath =
+ "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)";
+ transformOrigin = "center center";
+ } else {
+ // rectangle / square (Konva Rect uses x/y as top-left).
+ leftPx = nodeX;
+ topPx = nodeY;
+ widthPx = (obj.width ?? 100) * scale;
+ heightPx = (obj.height ?? 100) * scale;
+ borderRadiusPx = Math.max(0, obj.borderRadius ?? 0) * scale;
+ transformOrigin = "0% 0%";
+ }
+
+ const objOpacity = obj.opacity ?? 100; // 0-100 (editor convention)
+ // Make blur tint translucent so the backdrop blur stays visible.
+ const BLUR_TINT_OPACITY_FACTOR = 0.25;
+ const tintOpacity = Math.max(0, Math.min(100, objOpacity * BLUR_TINT_OPACITY_FACTOR));
+ const defaultFill = "#3b82f6";
+
+ const overlayBackground: CSSProperties =
+ obj.fillType === "gradient" && obj.gradient
+ ? (() => {
+ const g = obj.gradient as LinearGradient;
+ const angle = toCssGradientAngle(g.angle);
+ const from = getColorWithOpacity(g.from, tintOpacity);
+ const to = getColorWithOpacity(g.to, tintOpacity);
+ return {
+ backgroundImage: `linear-gradient(${angle}deg, ${from} 0%, ${to} 100%)`,
+ backgroundColor: "transparent",
+ };
+ })()
+ : {
+ backgroundColor: getColorWithOpacity(obj.fill ?? defaultFill, tintOpacity),
+ };
+
+ const style: CSSProperties = {
+ position: "absolute",
+ left: `${leftPx}px`,
+ top: `${topPx}px`,
+ width: `${widthPx}px`,
+ height: `${heightPx}px`,
+ zIndex: index,
+ pointerEvents: "none",
+ backdropFilter: `blur(${blurPx}px)`,
+ WebkitBackdropFilter: `blur(${blurPx}px)`,
+ ...overlayBackground,
+ overflow: clipPath ? "hidden" : borderRadiusPx > 0 ? "hidden" : undefined,
+ borderRadius: borderRadiusPx > 0 ? `${borderRadiusPx}px` : undefined,
+ clipPath,
+ transform: rotation ? `rotate(${rotation}deg)` : undefined,
+ transformOrigin,
+ };
+
+ return ;
+ })
+ ;
+ }, [objects, scale, dragRevision, stageRef]);
+
+ return (
+
+ {overlays}
+
+ );
+};
+
+export default BlurBackdropLayer;
+
diff --git a/src/pages/editor/components/tools/AbstractShape.tsx b/src/pages/editor/components/tools/AbstractShape.tsx
index 8f0bf6d..33bfd9f 100644
--- a/src/pages/editor/components/tools/AbstractShape.tsx
+++ b/src/pages/editor/components/tools/AbstractShape.tsx
@@ -3,7 +3,8 @@ import { Star } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
-import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
+import { getColorWithOpacity } from "../../utils/colorOpacity";
+import { useShapeBlurCache } from "../../utils/shapeBlur";
const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -19,9 +20,13 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
- const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
+ const blurRadius = obj.blur ?? 0;
+ const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
+ // Editor blur is implemented via backdrop-filter (see BlurBackdropLayer).
+ // Avoid applying Konva blur filters on the element itself.
+ const blurProps = {};
- useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ useShapeBlurCache(shapeRef, 0, [
obj.width,
obj.height,
obj.fill,
@@ -29,7 +34,6 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
obj.gradient,
obj.stroke,
actualStrokeWidth,
- obj.blur,
]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
@@ -44,14 +48,14 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
const displayStrokeWidth = isSelected
? (hasStroke ? actualStrokeWidth : 3)
: (isMask && showGuide ? 2 : actualStrokeWidth);
- const gradientProps = isMask
- ? {}
- : getKonvaGradientProps(
- obj.fillType,
- obj.gradient,
- obj.width || 100,
- obj.height || 100,
- "centered",
+ const gradientProps = isMask || blurRadius > 0
+ ? {}
+ : getKonvaGradientProps(
+ obj.fillType,
+ obj.gradient,
+ obj.width || 100,
+ obj.height || 100,
+ "centered",
);
return (
@@ -64,7 +68,8 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
numPoints={5}
innerRadius={innerRadius}
outerRadius={radius}
- fill={isMask ? "transparent" : obj.fill}
+ // When blur is enabled, tint is rendered by BlurBackdropLayer.
+ fill={isMask ? "transparent" : blurRadius > 0 ? blurHitFill : (obj.fill ?? "#000000")}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
diff --git a/src/pages/editor/components/tools/CircleShape.tsx b/src/pages/editor/components/tools/CircleShape.tsx
index 4b37697..6d4e7f2 100644
--- a/src/pages/editor/components/tools/CircleShape.tsx
+++ b/src/pages/editor/components/tools/CircleShape.tsx
@@ -3,7 +3,8 @@ import { Circle } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
-import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
+import { getColorWithOpacity } from "../../utils/colorOpacity";
+import { useShapeBlurCache } from "../../utils/shapeBlur";
const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -15,16 +16,19 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
- const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
+ const blurRadius = obj.blur ?? 0;
+ const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
+ // Editor blur is implemented via backdrop-filter (see BlurBackdropLayer).
+ // Avoid applying Konva blur filters on the element itself.
+ const blurProps = {};
- useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ useShapeBlurCache(shapeRef, 0, [
obj.width,
obj.fill,
obj.fillType,
obj.gradient,
obj.stroke,
actualStrokeWidth,
- obj.blur,
]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
@@ -40,9 +44,9 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
? (hasStroke ? actualStrokeWidth : 3)
: (isMask && showGuide ? 2 : actualStrokeWidth);
const diameter = obj.width || 100;
- const gradientProps = isMask
- ? {}
- : getKonvaGradientProps(obj.fillType, obj.gradient, diameter, diameter, "centered");
+ const gradientProps = isMask || blurRadius > 0
+ ? {}
+ : getKonvaGradientProps(obj.fillType, obj.gradient, diameter, diameter, "centered");
return (
0 ? blurHitFill : (obj.fill ?? "#000000")}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
diff --git a/src/pages/editor/components/tools/RectangleShape.tsx b/src/pages/editor/components/tools/RectangleShape.tsx
index 90aa1b5..dbe3abd 100644
--- a/src/pages/editor/components/tools/RectangleShape.tsx
+++ b/src/pages/editor/components/tools/RectangleShape.tsx
@@ -3,7 +3,8 @@ import { Rect } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
-import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
+import { getColorWithOpacity } from "../../utils/colorOpacity";
+import { useShapeBlurCache } from "../../utils/shapeBlur";
const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -16,9 +17,13 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
const cornerRadius = Math.max(0, obj.borderRadius ?? 0);
- const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
+ const blurRadius = obj.blur ?? 0;
+ const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
+ // Editor blur is implemented via backdrop-filter (see BlurBackdropLayer),
+ // so we intentionally do not apply Konva blur filters here.
+ const blurProps = {};
- useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ useShapeBlurCache(shapeRef, 0, [
obj.width,
obj.height,
cornerRadius,
@@ -27,7 +32,6 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
obj.gradient,
obj.stroke,
actualStrokeWidth,
- obj.blur,
]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
@@ -42,7 +46,7 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
const displayStrokeWidth = isSelected
? (hasStroke ? actualStrokeWidth : 3)
: (isMask && showGuide ? 2 : actualStrokeWidth);
- const gradientProps = isMask
+ const gradientProps = isMask || blurRadius > 0
? {}
: getKonvaGradientProps(
obj.fillType,
@@ -62,7 +66,9 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
width={obj.width || 100}
height={obj.height || 100}
cornerRadius={cornerRadius}
- fill={isMask ? "transparent" : obj.fill}
+ // When blur is enabled, visual tint is rendered by BlurBackdropLayer.
+ // Use a tiny non-zero alpha so Konva hit-detection still works for dragging.
+ fill={isMask ? "transparent" : blurRadius > 0 ? blurHitFill : (obj.fill ?? "#000000")}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
diff --git a/src/pages/editor/components/tools/TriangleShape.tsx b/src/pages/editor/components/tools/TriangleShape.tsx
index dd19118..41637b1 100644
--- a/src/pages/editor/components/tools/TriangleShape.tsx
+++ b/src/pages/editor/components/tools/TriangleShape.tsx
@@ -3,7 +3,8 @@ import { RegularPolygon } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
-import { getKonvaBlurProps, useShapeBlurCache } from "../../utils/shapeBlur";
+import { getColorWithOpacity } from "../../utils/colorOpacity";
+import { useShapeBlurCache } from "../../utils/shapeBlur";
const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -16,17 +17,20 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
- const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
+ const blurRadius = obj.blur ?? 0;
+ const blurHitFill = blurRadius > 0 ? getColorWithOpacity(obj.fill ?? "#000000", 0.5) : undefined;
+ // Editor blur is implemented via backdrop-filter (see BlurBackdropLayer).
+ // Avoid applying Konva blur filters on the element itself.
+ const blurProps = {};
- useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ useShapeBlurCache(shapeRef, 0, [
obj.width,
- obj.height,
obj.fill,
obj.fillType,
obj.gradient,
obj.stroke,
actualStrokeWidth,
- obj.blur,
+ obj.height,
]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
@@ -41,14 +45,14 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
const displayStrokeWidth = isSelected
? (hasStroke ? actualStrokeWidth : 3)
: (isMask && showGuide ? 2 : actualStrokeWidth);
- const gradientProps = isMask
- ? {}
- : getKonvaGradientProps(
- obj.fillType,
- obj.gradient,
- obj.width || 100,
- obj.height || 100,
- "centered",
+ const gradientProps = isMask || blurRadius > 0
+ ? {}
+ : getKonvaGradientProps(
+ obj.fillType,
+ obj.gradient,
+ obj.width || 100,
+ obj.height || 100,
+ "centered",
);
return (
@@ -60,7 +64,8 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
y={obj.y + (obj.height || 100) / 2}
sides={3}
radius={radius}
- fill={isMask ? "transparent" : obj.fill}
+ // When blur is enabled, tint is rendered by BlurBackdropLayer.
+ fill={isMask ? "transparent" : blurRadius > 0 ? blurHitFill : (obj.fill ?? "#000000")}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}