@@ -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(() => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm">بلور</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={50}
|
||||
value={baseBlur}
|
||||
onChange={(e) =>
|
||||
onUpdate(selectedObject.id, {
|
||||
blur: Math.max(0, Math.min(50, Number(e.target.value))),
|
||||
})
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={50}
|
||||
value={baseBlur}
|
||||
onChange={(e) =>
|
||||
onUpdate(selectedObject.id, {
|
||||
blur: Math.max(0, Math.min(50, Number(e.target.value) || 0)),
|
||||
})
|
||||
}
|
||||
className="w-16 h-9 rounded-lg border border-border px-2 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Konva.Star>(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) {
|
||||
|
||||
@@ -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<Konva.Circle>(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) {
|
||||
|
||||
@@ -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<Konva.Rect>(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) {
|
||||
|
||||
@@ -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<Konva.RegularPolygon>(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) {
|
||||
|
||||
@@ -82,6 +82,8 @@ export type EditorObject = {
|
||||
scaleY?: number;
|
||||
shapeType?: ShapeType;
|
||||
borderRadius?: number;
|
||||
/** میزان بلور شکل به پیکسل (۰ = بدون بلور) */
|
||||
blur?: number;
|
||||
tableData?: TableData;
|
||||
visible?: boolean;
|
||||
isMask?: boolean;
|
||||
|
||||
@@ -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<Konva.Shape | null>,
|
||||
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]);
|
||||
}
|
||||
@@ -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<HTMLDivElement, BookPageProps>(
|
||||
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<HTMLDivElement, BookPageProps>(
|
||||
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<HTMLDivElement, BookPageProps>(
|
||||
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<HTMLDivElement, BookPageProps>(
|
||||
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<HTMLDivElement, BookPageProps>(
|
||||
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}`
|
||||
|
||||
@@ -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") &&
|
||||
|
||||
Reference in New Issue
Block a user