grediant in a shape

This commit is contained in:
hamid zarghami
2026-05-09 09:50:13 +03:30
parent 0229a1355d
commit b513ecb33f
16 changed files with 378 additions and 15 deletions
+11 -1
View File
@@ -17,6 +17,7 @@ import CellEditor from "@/components/CellEditor";
import ZoomControls from "./ZoomControls";
import EditorCanvasToolbar from "./EditorCanvasToolbar";
import { createScopedId } from "../store/editorStore.helpers";
import { getKonvaGradientProps } from "../utils/gradient";
type EditorCanvasProps = {
catalogSize?: string;
@@ -46,9 +47,10 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
const currentPage = pages.find((page) => page.id === currentPageId);
const bgColor =
currentPage?.backgroundType === 'color'
currentPage?.backgroundType === 'color' || currentPage?.backgroundType === 'gradient'
? currentPage.backgroundColor
: '#ffffff';
const bgGradient = currentPage?.backgroundGradient;
const [bgImage] = useImage(
currentPage?.backgroundType === 'image'
@@ -331,6 +333,13 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
const scaledW = stageSize.width * finalScale;
const scaledH = stageSize.height * finalScale;
const backgroundGradientProps = getKonvaGradientProps(
currentPage?.backgroundType === "gradient" ? "gradient" : "solid",
bgGradient,
stageSize.width,
stageSize.height,
"rect",
);
return (
<div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-auto pb-8 xl:pl-6">
@@ -394,6 +403,7 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
width={stageSize.width}
height={stageSize.height}
fill={bgColor}
{...backgroundGradientProps}
listening={false}
/>
{currentPage?.backgroundType === 'image' && bgImage && (
+88 -1
View File
@@ -5,6 +5,7 @@ import type { DisplayStyle, BackgroundType } from '../store/editorStore'
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
import { useSingleUpload } from '@/pages/uploader/hooks/useUploaderData'
import ColorsImage from '@/assets/images/colors.png'
import ColorPicker from '@/components/ColorPicker'
const PRESET_COLORS = [
'#a8edcf',
@@ -23,6 +24,7 @@ const DISPLAY_STYLE_OPTIONS: { value: DisplayStyle; label: string }[] = [
const BACKGROUND_TYPE_OPTIONS: { value: BackgroundType; label: string }[] = [
{ value: 'color', label: 'رنگ' },
{ value: 'gradient', label: 'گرادیانت' },
{ value: 'image', label: 'تصویر' },
]
@@ -39,6 +41,11 @@ const SettingsPanel = () => {
const currentPage = pages.find((page) => page.id === currentPageId)
const backgroundType = currentPage?.backgroundType ?? 'color'
const backgroundColor = currentPage?.backgroundColor ?? '#ffffff'
const backgroundGradient = currentPage?.backgroundGradient ?? {
from: '#ffffff',
to: '#e2e8f0',
angle: 135,
}
const backgroundImageUrl = currentPage?.backgroundImageUrl ?? ''
const [uploadedPreviews, setUploadedPreviews] = useState<string[]>(
backgroundImageUrl ? [backgroundImageUrl] : []
@@ -145,7 +152,20 @@ const SettingsPanel = () => {
<label key={opt.value} className="flex items-center gap-1.5 cursor-pointer select-none">
<span className="text-xs text-gray-600">{opt.label}</span>
<div
onClick={() => updateCurrentPageBackground({ backgroundType: opt.value })}
onClick={() =>
updateCurrentPageBackground({
backgroundType: opt.value,
...(opt.value === 'gradient'
? {
backgroundGradient: currentPage?.backgroundGradient ?? {
from: '#ffffff',
to: '#e2e8f0',
angle: 135,
},
}
: {}),
})
}
className={clx(
'w-4 h-4 rounded-full border-2 flex items-center justify-center transition-colors cursor-pointer',
backgroundType === opt.value
@@ -209,6 +229,73 @@ const SettingsPanel = () => {
</div>
)}
{backgroundType === 'gradient' && (
<div className="space-y-3">
<p className="text-xs text-gray-500">گرادیانت خطی (سبک Figma)</p>
<ColorPicker
label="رنگ شروع"
value={backgroundGradient.from}
onChange={(value) =>
updateCurrentPageBackground({
backgroundGradient: { ...backgroundGradient, from: value },
})
}
/>
<ColorPicker
label="رنگ پایان"
value={backgroundGradient.to}
onChange={(value) =>
updateCurrentPageBackground({
backgroundGradient: { ...backgroundGradient, to: value },
})
}
/>
<div className="space-y-1">
<label className="text-xs text-gray-500">زاویه</label>
<div className="flex items-center gap-2">
<input
type="range"
min={0}
max={360}
value={backgroundGradient.angle}
onChange={(e) =>
updateCurrentPageBackground({
backgroundGradient: {
...backgroundGradient,
angle: Number(e.target.value),
},
})
}
className="w-full"
/>
<input
type="number"
min={0}
max={360}
value={backgroundGradient.angle}
onChange={(e) =>
updateCurrentPageBackground({
backgroundGradient: {
...backgroundGradient,
angle: Number(e.target.value) || 0,
},
})
}
className="w-16 h-9 rounded-lg border border-border px-2 text-xs"
/>
</div>
</div>
<div
className="h-12 rounded-xl border border-border"
style={{
backgroundImage: `linear-gradient(${backgroundGradient.angle}deg, ${backgroundGradient.from} 0%, ${backgroundGradient.to} 100%)`,
}}
/>
</div>
)}
{/* ─── آپلود تصویر ─── */}
{backgroundType === 'image' && (
<div>
@@ -74,7 +74,7 @@ const ObjectRenderer = ({
if (rafId1) cancelAnimationFrame(rafId1);
groupNode?.clearCache();
};
}, [shouldApplyMask, isSelected, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill]);
}, [shouldApplyMask, isSelected, maskShape?.x, maskShape?.y, maskShape?.width, maskShape?.height, maskShape?.rotation, obj.x, obj.y, obj.width, obj.height, obj.rotation, obj.maskInvert, obj.strokeWidth, obj.stroke, obj.fill, obj.fillType, obj.gradient]);
// Refresh cache after transformer is attached (when isSelected changes)
useEffect(() => {
@@ -11,6 +11,12 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
const baseWidth = selectedObject.width ?? 100;
const baseHeight = selectedObject.height ?? 100;
const baseFill = selectedObject.fill ?? "#3b82f6";
const fillType = selectedObject.fillType ?? "solid";
const gradient = selectedObject.gradient ?? {
from: "#3b82f6",
to: "#1d4ed8",
angle: 135,
};
const baseStroke = selectedObject.stroke ?? "#1e40af";
const baseStrokeWidth = selectedObject.strokeWidth ?? 0;
const baseBorderRadius = selectedObject.borderRadius ?? 0;
@@ -42,12 +48,84 @@ const ShapeSettings = ({ selectedObject, onUpdate }: ShapeSettingsProps) => {
<ColorPicker
label="رنگ پس‌زمینه"
value={baseFill}
readOnly={fillType === "gradient"}
onChange={(value) =>
onUpdate(selectedObject.id, {
fill: value,
})
}
/>
<div className="space-y-2">
<label className="text-sm">نوع Fill</label>
<div className="flex gap-2">
<button
type="button"
onClick={() =>
onUpdate(selectedObject.id, {
fillType: "solid",
})
}
className={`px-3 py-1.5 rounded-lg border text-xs ${fillType === "solid" ? "border-gray-800 text-gray-900" : "border-border text-gray-500"
}`}
>
رنگ ساده
</button>
<button
type="button"
onClick={() =>
onUpdate(selectedObject.id, {
fillType: "gradient",
gradient: selectedObject.gradient ?? {
from: "#3b82f6",
to: "#1d4ed8",
angle: 135,
},
})
}
className={`px-3 py-1.5 rounded-lg border text-xs ${fillType === "gradient" ? "border-gray-800 text-gray-900" : "border-border text-gray-500"
}`}
>
گرادیانت
</button>
</div>
</div>
{fillType === "gradient" && (
<div className="space-y-3 border border-border rounded-xl p-3">
<ColorPicker
label="رنگ شروع"
value={gradient.from}
onChange={(value) =>
onUpdate(selectedObject.id, {
gradient: { ...gradient, from: value },
})
}
/>
<ColorPicker
label="رنگ پایان"
value={gradient.to}
onChange={(value) =>
onUpdate(selectedObject.id, {
gradient: { ...gradient, to: value },
})
}
/>
<Input
label="زاویه گرادیانت"
type="number"
value={gradient.angle}
onChange={(e) =>
onUpdate(selectedObject.id, {
gradient: {
...gradient,
angle: Number(e.target.value) || 0,
},
})
}
min={0}
max={360}
/>
</div>
)}
<ColorPicker
label="رنگ خط"
value={baseStroke}
@@ -2,6 +2,7 @@ import { useRef } from "react";
import { Star } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Star>(null);
@@ -30,6 +31,15 @@ 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",
);
return (
<Star
@@ -42,6 +52,7 @@ const AbstractShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
innerRadius={innerRadius}
outerRadius={radius}
fill={isMask ? "transparent" : obj.fill}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}
@@ -2,6 +2,7 @@ import { useRef } from "react";
import { Circle } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Circle>(null);
@@ -26,6 +27,10 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
const displayStrokeWidth = isSelected
? (hasStroke ? actualStrokeWidth : 3)
: (isMask && showGuide ? 2 : actualStrokeWidth);
const diameter = obj.width || 100;
const gradientProps = isMask
? {}
: getKonvaGradientProps(obj.fillType, obj.gradient, diameter, diameter, "centered");
return (
<Circle
@@ -36,6 +41,7 @@ const CircleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapePr
y={obj.y}
radius={(obj.width || 50) / 2}
fill={isMask ? "transparent" : obj.fill}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}
@@ -2,6 +2,7 @@ import { useRef } from "react";
import { Rect } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Rect>(null);
@@ -27,6 +28,15 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
const displayStrokeWidth = isSelected
? (hasStroke ? actualStrokeWidth : 3)
: (isMask && showGuide ? 2 : actualStrokeWidth);
const gradientProps = isMask
? {}
: getKonvaGradientProps(
obj.fillType,
obj.gradient,
obj.width || 100,
obj.height || 100,
"rect",
);
return (
<Rect
@@ -39,6 +49,7 @@ const RectangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shap
height={obj.height || 100}
cornerRadius={cornerRadius}
fill={isMask ? "transparent" : obj.fill}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}
@@ -2,6 +2,7 @@ import { useRef } from "react";
import { RegularPolygon } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
import { getKonvaGradientProps } from "../../utils/gradient";
const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.RegularPolygon>(null);
@@ -27,6 +28,15 @@ 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",
);
return (
<RegularPolygon
@@ -38,6 +48,7 @@ const TriangleShape = ({ obj, isSelected, onSelect, onUpdate, draggable }: Shape
sides={3}
radius={radius}
fill={isMask ? "transparent" : obj.fill}
{...gradientProps}
stroke={displayStroke}
strokeWidth={displayStrokeWidth}
dash={isMask && showGuide ? [10, 5] : undefined}