Create shapes square + circle + ....

This commit is contained in:
hamid zarghami
2025-11-18 15:31:09 +03:30
parent 9b06c7dfc5
commit 3f18ac62fa
9 changed files with 239 additions and 50 deletions
+4
View File
@@ -85,3 +85,7 @@ html {
.dltr { .dltr {
direction: ltr; direction: ltr;
} }
.filterBlack {
filter: brightness(0) invert(0);
}
@@ -3,6 +3,8 @@ import type { EditorObject } from "../../store/editorStore";
import { import {
RectangleShape, RectangleShape,
CircleShape, CircleShape,
TriangleShape,
AbstractShape,
TextShape, TextShape,
LineShape, LineShape,
ArrowShape, ArrowShape,
@@ -41,6 +43,15 @@ const ObjectRenderer = ({
switch (obj.type) { switch (obj.type) {
case "rectangle": case "rectangle":
if (obj.shapeType === "circle") {
return <CircleShape key={obj.id} {...commonProps} />;
}
if (obj.shapeType === "triangle") {
return <TriangleShape key={obj.id} {...commonProps} />;
}
if (obj.shapeType === "abstract") {
return <AbstractShape key={obj.id} {...commonProps} />;
}
return <RectangleShape key={obj.id} {...commonProps} />; return <RectangleShape key={obj.id} {...commonProps} />;
case "circle": case "circle":
return <CircleShape key={obj.id} {...commonProps} />; return <CircleShape key={obj.id} {...commonProps} />;
@@ -1,4 +1,5 @@
import type { EditorObject, ToolType } from "../../store/editorStore"; import type { EditorObject, ToolType } from "../../store/editorStore";
import { useShapeStore } from "../../store/shapeStore";
type DrawingParams = { type DrawingParams = {
tool: ToolType; tool: ToolType;
@@ -17,6 +18,7 @@ export const createDrawingObject = ({
const height = pointerPos.y - startPos.y; const height = pointerPos.y - startPos.y;
if (tool === "rectangle") { if (tool === "rectangle") {
const activeShape = useShapeStore.getState().activeShape;
return { return {
id: tempObject?.id || `rect-${Date.now()}`, id: tempObject?.id || `rect-${Date.now()}`,
type: "rectangle", type: "rectangle",
@@ -27,6 +29,7 @@ export const createDrawingObject = ({
fill: "#3b82f6", fill: "#3b82f6",
stroke: "#1e40af", stroke: "#1e40af",
strokeWidth: 2, strokeWidth: 2,
shapeType: activeShape,
}; };
} }
@@ -1,53 +1,54 @@
import { type FC } from 'react' import { type FC } from "react";
import SquareIcon from '@/assets/icons/square.svg' import SquareIcon from "@/assets/icons/square.svg";
import CircleIcon from '@/assets/icons/circle.svg' import CircleIcon from "@/assets/icons/circle.svg";
import TriangleIcon from '@/assets/icons/triangle.svg' import TriangleIcon from "@/assets/icons/triangle.svg";
import StarIcon from '@/assets/icons/Star.svg' import StarIcon from "@/assets/icons/Star.svg";
import { Switch } from '@/components/ui/switch' import { Switch } from "@/components/ui/switch";
import { clx } from "@/helpers/utils";
import { useShapeStore, type ShapeType } from "@/pages/editor/store/shapeStore";
const RectangleInstruction: FC = () => ( const shapeOptions: Array<{ id: ShapeType; label: string; icon: string; alt: string }> = [
<div> { id: "square", label: "مربع", icon: SquareIcon, alt: "square" },
<h2 className='font-bold'> { id: "circle", label: "دایره", icon: CircleIcon, alt: "circle" },
اشکال { id: "triangle", label: "مثلث", icon: TriangleIcon, alt: "triangle" },
</h2> { id: "abstract", label: "انتزاعی", icon: StarIcon, alt: "abstract" },
];
<div className='flex flex-wrap mt-7'> const RectangleInstruction: FC = () => {
<div className='size-[60px] flex flex-col gap-1 justify-center items-center'> const { activeShape, setActiveShape } = useShapeStore();
<img src={SquareIcon} alt='square' className='w-6' />
<div className='text-[13px]'> return (
مربع <div>
</div> <h2 className="font-bold">اشکال</h2>
<div className="mt-7 flex flex-wrap gap-3 text-[#B2B2B2]">
{shapeOptions.map((shape) => {
const isActive = activeShape === shape.id;
return (
<button
type="button"
key={shape.id}
onClick={() => setActiveShape(shape.id)}
className={clx(
"size-[60px] flex flex-col gap-1 justify-center items-center rounded-xl text-[13px] transition-colors",
isActive ? "border-black text-black" : "border-transparent hover:border-black/20",
)}
aria-pressed={isActive}
>
<img src={shape.icon} alt={shape.alt} className={clx("w-6", isActive && "filterBlack")} />
<div className={clx(isActive ? "text-black" : "text-[#7A7A7A]")}>{shape.label}</div>
</button>
);
})}
</div> </div>
<div className='size-[60px] flex flex-col gap-1 justify-center items-center'>
<img src={CircleIcon} alt='circle' className='w-6' /> <div className="mt-9 flex items-center justify-between">
<div className='text-[13px]'> <div className="text-[13px]">استفاده برای ماسک</div>
دایره <Switch />
</div>
</div>
<div className='size-[60px] flex flex-col gap-1 justify-center items-center'>
<img src={TriangleIcon} alt='triangle' className='w-6' />
<div className='text-[13px]'>
مثلث
</div>
</div>
<div className='size-[60px] flex flex-col gap-1 justify-center items-center'>
<img src={StarIcon} alt='star' className='w-6' />
<div className='text-[13px]'>
انتزاعی
</div>
</div> </div>
</div> </div>
);
<div className='mt-9 flex justify-between items-center'> };
<div className='text-[13px]'>
استفاده برای ماسک
</div>
<Switch
/>
</div>
</div>
);
export default RectangleInstruction; export default RectangleInstruction;
@@ -0,0 +1,69 @@
import { useEffect, useRef } from "react";
import { Star } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
const AbstractShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.Star>(null);
useEffect(() => {
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const radius = Math.min((obj.width || 100) / 2, (obj.height || 100) / 2);
const innerRadius = radius * 0.5;
return (
<Star
ref={shapeRef}
x={obj.x + (obj.width || 100) / 2}
y={obj.y + (obj.height || 100) / 2}
numPoints={5}
innerRadius={innerRadius}
outerRadius={radius}
fill={obj.fill}
stroke={isSelected ? "#3b82f6" : obj.stroke}
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
rotation={obj.rotation || 0}
draggable={draggable}
onClick={() => {
if (shapeRef.current) {
onSelect(obj.id, shapeRef.current);
}
}}
onDragEnd={(e) => {
const node = e.target;
onUpdate(obj.id, {
x: node.x() - (obj.width || 100) / 2,
y: node.y() - (obj.height || 100) / 2,
});
}}
onTransformEnd={() => {
const node = shapeRef.current;
if (!node) return;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
node.scaleY(1);
const newRadius = Math.max(5, radius * Math.min(scaleX, scaleY));
const newSize = newRadius * 2;
onUpdate(obj.id, {
x: node.x() - newSize / 2,
y: node.y() - newSize / 2,
rotation: node.rotation(),
width: newSize,
height: newSize,
});
}}
/>
);
};
export default AbstractShape;
@@ -0,0 +1,67 @@
import { useEffect, useRef } from "react";
import { RegularPolygon } from "react-konva";
import Konva from "konva";
import type { ShapeProps } from "./types";
const TriangleShape = ({ obj, isSelected, transformerRef, onSelect, onUpdate, draggable }: ShapeProps) => {
const shapeRef = useRef<Konva.RegularPolygon>(null);
useEffect(() => {
if (isSelected && shapeRef.current && transformerRef.current) {
transformerRef.current.nodes([shapeRef.current]);
transformerRef.current.getLayer()?.batchDraw();
}
}, [isSelected, transformerRef]);
const radius = Math.min((obj.width || 100) / 2, (obj.height || 100) / 2);
return (
<RegularPolygon
ref={shapeRef}
x={obj.x + (obj.width || 100) / 2}
y={obj.y + (obj.height || 100) / 2}
sides={3}
radius={radius}
fill={obj.fill}
stroke={isSelected ? "#3b82f6" : obj.stroke}
strokeWidth={isSelected ? 3 : obj.strokeWidth || 2}
rotation={obj.rotation || 0}
draggable={draggable}
onClick={() => {
if (shapeRef.current) {
onSelect(obj.id, shapeRef.current);
}
}}
onDragEnd={(e) => {
const node = e.target;
onUpdate(obj.id, {
x: node.x() - (obj.width || 100) / 2,
y: node.y() - (obj.height || 100) / 2,
});
}}
onTransformEnd={() => {
const node = shapeRef.current;
if (!node) return;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
node.scaleY(1);
const newRadius = Math.max(5, radius * Math.min(scaleX, scaleY));
const newSize = newRadius * 2;
onUpdate(obj.id, {
x: node.x() - newSize / 2,
y: node.y() - newSize / 2,
rotation: node.rotation(),
width: newSize,
height: newSize,
});
}}
/>
);
};
export default TriangleShape;
@@ -1,5 +1,7 @@
export { default as RectangleShape } from "./RectangleShape"; export { default as RectangleShape } from "./RectangleShape";
export { default as CircleShape } from "./CircleShape"; export { default as CircleShape } from "./CircleShape";
export { default as TriangleShape } from "./TriangleShape";
export { default as AbstractShape } from "./AbstractShape";
export { default as TextShape } from "./TextShape"; export { default as TextShape } from "./TextShape";
export { default as LineShape } from "./LineShape"; export { default as LineShape } from "./LineShape";
export { default as ArrowShape } from "./ArrowShape"; export { default as ArrowShape } from "./ArrowShape";
+22 -5
View File
@@ -1,7 +1,20 @@
import { create } from "zustand"; import { create } from "zustand";
import Konva from "konva"; import Konva from "konva";
import type { ShapeType } from "./shapeStore";
export type ToolType = "select" | "rectangle" | "circle" | "text" | "image" | "line" | "arrow" | "sticker" | "document" | "video" | "link" | "grid"; export type ToolType =
| "select"
| "rectangle"
| "circle"
| "text"
| "image"
| "line"
| "arrow"
| "sticker"
| "document"
| "video"
| "link"
| "grid";
export type EditorObject = { export type EditorObject = {
id: string; id: string;
@@ -21,6 +34,7 @@ export type EditorObject = {
rotation?: number; rotation?: number;
scaleX?: number; scaleX?: number;
scaleY?: number; scaleY?: number;
shapeType?: ShapeType;
}; };
type EditorStoreType = { type EditorStoreType = {
@@ -42,15 +56,19 @@ export const useEditorStore = create<EditorStoreType>((set) => ({
tool: "select", tool: "select",
setTool: (tool) => set({ tool }), setTool: (tool) => set({ tool }),
objects: [], objects: [],
addObject: (object) => set((state) => ({ objects: [...state.objects, object] })), addObject: (object) =>
set((state) => ({ objects: [...state.objects, object] })),
updateObject: (id, updates) => updateObject: (id, updates) =>
set((state) => ({ set((state) => ({
objects: state.objects.map((obj) => (obj.id === id ? { ...obj, ...updates } : obj)), objects: state.objects.map((obj) =>
obj.id === id ? { ...obj, ...updates } : obj
),
})), })),
deleteObject: (id) => deleteObject: (id) =>
set((state) => ({ set((state) => ({
objects: state.objects.filter((obj) => obj.id !== id), objects: state.objects.filter((obj) => obj.id !== id),
selectedObjectId: state.selectedObjectId === id ? null : state.selectedObjectId, selectedObjectId:
state.selectedObjectId === id ? null : state.selectedObjectId,
})), })),
selectedObjectId: null, selectedObjectId: null,
setSelectedObjectId: (id) => set({ selectedObjectId: id }), setSelectedObjectId: (id) => set({ selectedObjectId: id }),
@@ -59,4 +77,3 @@ export const useEditorStore = create<EditorStoreType>((set) => ({
layerRef: null, layerRef: null,
setLayerRef: (ref) => set({ layerRef: ref }), setLayerRef: (ref) => set({ layerRef: ref }),
})); }));
+15
View File
@@ -0,0 +1,15 @@
import { create } from "zustand";
export type ShapeType = "square" | "circle" | "triangle" | "abstract";
type ShapeStore = {
activeShape: ShapeType;
setActiveShape: (shape: ShapeType) => void;
};
export const useShapeStore = create<ShapeStore>((set) => ({
activeShape: "square",
setActiveShape: (shape) => set({ activeShape: shape }),
}));