diff --git a/src/pages/editor/components/canvas/ObjectRenderer.tsx b/src/pages/editor/components/canvas/ObjectRenderer.tsx
index d66f143..06ef848 100644
--- a/src/pages/editor/components/canvas/ObjectRenderer.tsx
+++ b/src/pages/editor/components/canvas/ObjectRenderer.tsx
@@ -88,7 +88,7 @@ const ObjectRenderer = ({
groupNode?.clearCache();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [shouldApplyMask, isSelected, maskRelXDep, maskRelYDep, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill, obj.fillType, obj.gradient]);
+ }, [shouldApplyMask, isSelected, maskRelXDep, maskRelYDep, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill, obj.fillType, obj.gradient, obj.blur]);
// Refresh cache after transformer is attached (when isSelected changes)
useEffect(() => {
diff --git a/src/pages/editor/components/sidebar/settings/ShapeSettings.tsx b/src/pages/editor/components/sidebar/settings/ShapeSettings.tsx
index fc39210..c623bec 100644
--- a/src/pages/editor/components/sidebar/settings/ShapeSettings.tsx
+++ b/src/pages/editor/components/sidebar/settings/ShapeSettings.tsx
@@ -20,6 +20,7 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
const baseStroke = selectedObject.stroke ?? "#1e40af";
const baseStrokeWidth = selectedObject.strokeWidth ?? 0;
const baseBorderRadius = selectedObject.borderRadius ?? 0;
+ const baseBlur = selectedObject.blur ?? 0;
const isSquareShape =
selectedObject.shapeType === "square" || selectedObject.shapeType === undefined;
@@ -179,6 +180,35 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
min={0}
/>
)}
+
);
};
diff --git a/src/pages/editor/components/tools/AbstractShape.tsx b/src/pages/editor/components/tools/AbstractShape.tsx
index b20f085..bfc3598 100644
--- a/src/pages/editor/components/tools/AbstractShape.tsx
+++ b/src/pages/editor/components/tools/AbstractShape.tsx
@@ -3,6 +3,7 @@ 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";
const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -18,6 +19,18 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
+ const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
+
+ useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ obj.width,
+ obj.height,
+ obj.fill,
+ obj.fillType,
+ obj.gradient,
+ obj.stroke,
+ actualStrokeWidth,
+ obj.blur,
+ ]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
const displayStroke = isSelected
@@ -57,6 +70,7 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0}
+ {...blurProps}
draggable={draggable}
onClick={(e) => {
if (shapeRef.current) {
diff --git a/src/pages/editor/components/tools/CircleShape.tsx b/src/pages/editor/components/tools/CircleShape.tsx
index 7a6a1cb..cca3292 100644
--- a/src/pages/editor/components/tools/CircleShape.tsx
+++ b/src/pages/editor/components/tools/CircleShape.tsx
@@ -3,6 +3,7 @@ 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";
const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -14,6 +15,17 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
+ const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
+
+ useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ obj.width,
+ obj.fill,
+ obj.fillType,
+ obj.gradient,
+ obj.stroke,
+ actualStrokeWidth,
+ obj.blur,
+ ]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
const displayStroke = isSelected
@@ -46,6 +58,7 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0}
+ {...blurProps}
draggable={draggable}
onClick={(e) => {
if (shapeRef.current) {
diff --git a/src/pages/editor/components/tools/RectangleShape.tsx b/src/pages/editor/components/tools/RectangleShape.tsx
index eee5bd4..6894414 100644
--- a/src/pages/editor/components/tools/RectangleShape.tsx
+++ b/src/pages/editor/components/tools/RectangleShape.tsx
@@ -3,6 +3,7 @@ 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";
const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -15,6 +16,19 @@ 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);
+
+ useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ obj.width,
+ obj.height,
+ cornerRadius,
+ obj.fill,
+ obj.fillType,
+ obj.gradient,
+ obj.stroke,
+ actualStrokeWidth,
+ obj.blur,
+ ]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
const displayStroke = isSelected
@@ -54,6 +68,7 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0}
+ {...blurProps}
draggable={draggable}
onClick={(e) => {
if (shapeRef.current) {
diff --git a/src/pages/editor/components/tools/TriangleShape.tsx b/src/pages/editor/components/tools/TriangleShape.tsx
index 6986e2e..6ac629c 100644
--- a/src/pages/editor/components/tools/TriangleShape.tsx
+++ b/src/pages/editor/components/tools/TriangleShape.tsx
@@ -3,6 +3,7 @@ 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";
const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef(null);
@@ -15,6 +16,18 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
const actualStrokeWidth = obj.strokeWidth ?? 0;
const hasStroke = actualStrokeWidth > 0;
+ const blurProps = isMask ? {} : getKonvaBlurProps(obj.blur);
+
+ useShapeBlurCache(shapeRef, isMask ? 0 : obj.blur, [
+ obj.width,
+ obj.height,
+ obj.fill,
+ obj.fillType,
+ obj.gradient,
+ obj.stroke,
+ actualStrokeWidth,
+ obj.blur,
+ ]);
// برای نمایش انتخاب: اگر strokeWidth واقعی > 0 است، از آن استفاده کن، وگرنه stroke را برای انتخاب نمایش بده
const displayStroke = isSelected
@@ -53,6 +66,7 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}
rotation={obj.rotation || 0}
+ {...blurProps}
draggable={draggable}
onClick={(e) => {
if (shapeRef.current) {
diff --git a/src/pages/editor/store/editorStore.types.ts b/src/pages/editor/store/editorStore.types.ts
index 19bf24b..ea9a041 100644
--- a/src/pages/editor/store/editorStore.types.ts
+++ b/src/pages/editor/store/editorStore.types.ts
@@ -82,6 +82,8 @@ export type EditorObject = {
scaleY?: number;
shapeType?: ShapeType;
borderRadius?: number;
+ /** میزان بلور شکل به پیکسل (۰ = بدون بلور) */
+ blur?: number;
tableData?: TableData;
visible?: boolean;
isMask?: boolean;
diff --git a/src/pages/editor/utils/shapeBlur.ts b/src/pages/editor/utils/shapeBlur.ts
new file mode 100644
index 0000000..db6c2aa
--- /dev/null
+++ b/src/pages/editor/utils/shapeBlur.ts
@@ -0,0 +1,42 @@
+import { useLayoutEffect, type CSSProperties } from "react";
+import type Konva from "konva";
+import KonvaLib from "konva";
+
+export function getKonvaBlurProps(blur?: number) {
+ const blurRadius = blur ?? 0;
+ if (blurRadius <= 0) return {};
+ return {
+ filters: [KonvaLib.Filters.Blur],
+ blurRadius,
+ };
+}
+
+export function getCssBlurStyle(blur?: number, scale = 1): CSSProperties {
+ const blurPx = blur ?? 0;
+ if (blurPx <= 0) return {};
+ return { filter: `blur(${blurPx * scale}px)` };
+}
+
+export function useShapeBlurCache(
+ shapeRef: React.RefObject,
+ blur: number | undefined,
+ cacheKey: unknown,
+) {
+ useLayoutEffect(() => {
+ const node = shapeRef.current;
+ if (!node) return;
+
+ const blurRadius = blur ?? 0;
+ if (blurRadius > 0) {
+ node.clearCache();
+ node.cache({
+ offset: blurRadius,
+ drawBorder: false,
+ });
+ } else {
+ node.clearCache();
+ }
+
+ node.getLayer()?.batchDraw();
+ }, [blur, cacheKey, shapeRef]);
+}
diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx
index b41823c..b3d9aaf 100644
--- a/src/pages/viewer/components/BookPage.tsx
+++ b/src/pages/viewer/components/BookPage.tsx
@@ -11,6 +11,7 @@ import {
resolveTextMaxWidth,
usesWrappedLayout,
} from '@/pages/editor/utils/textStyle';
+import { getCssBlurStyle } from '@/pages/editor/utils/shapeBlur';
import '@/pages/viewer/styles/entranceAnimations.css';
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
import { getMaskImageStyle, getMaskedLayout } from '@/pages/viewer/utils/maskStyle';
@@ -174,6 +175,7 @@ const BookPage = memo(forwardRef(
obj.fillType === 'gradient' && obj.gradient
? { backgroundImage: toCssLinearGradient(obj.gradient) }
: { backgroundColor: obj.fill || 'transparent' };
+ const shapeBlurStyle = getCssBlurStyle(obj.blur, scale);
switch (obj.type) {
case 'text': {
@@ -487,6 +489,7 @@ const BookPage = memo(forwardRef(
height: `${radius * 2}px`,
borderRadius: '50%',
...objectFillStyle,
+ ...shapeBlurStyle,
border: hasStroke && obj.stroke
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
: 'none',
@@ -549,6 +552,7 @@ const BookPage = memo(forwardRef(
width: `${size}px`,
height: `${size}px`,
opacity: (obj.opacity ?? 100) / 100,
+ ...shapeBlurStyle,
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
transformOrigin: 'center center',
zIndex: index,
@@ -608,6 +612,7 @@ const BookPage = memo(forwardRef(
height: `${radius * 2}px`,
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
...objectFillStyle,
+ ...shapeBlurStyle,
border: obj.stroke
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
: 'none',
@@ -630,6 +635,7 @@ const BookPage = memo(forwardRef(
width: `${(obj.width || 100) * scale}px`,
height: `${(obj.height || 100) * scale}px`,
...objectFillStyle,
+ ...shapeBlurStyle,
borderRadius: `${Math.max(0, (obj.borderRadius || 0) * scale)}px`,
border: hasStroke && obj.stroke
? `${actualStrokeWidth * scale}px solid ${obj.stroke}`
diff --git a/src/pages/viewer/utils/dataTransformer.ts b/src/pages/viewer/utils/dataTransformer.ts
index 7d932ca..d869901 100644
--- a/src/pages/viewer/utils/dataTransformer.ts
+++ b/src/pages/viewer/utils/dataTransformer.ts
@@ -38,6 +38,7 @@ type ViewerDataPage = {
strokeWidth?: number;
shapeType?: string;
borderRadius?: number;
+ blur?: number;
imageUrl?: string;
videoUrl?: string;
audioUrl?: string;
@@ -129,6 +130,9 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
if (obj.type === "rectangle" && obj.borderRadius !== undefined) {
baseObject.borderRadius = obj.borderRadius;
}
+ if (obj.type === "rectangle" && obj.blur !== undefined) {
+ baseObject.blur = obj.blur;
+ }
if (
(obj.type === "image" || obj.type === "sticker" || obj.type === "document") &&