added text section design layout
This commit is contained in:
parent
976978aec4
commit
954ac950b0
21 changed files with 2156 additions and 1822 deletions
12
src/App.jsx
12
src/App.jsx
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
// import Canvas from "./components/Canvas";
|
// import Canvas from "./components/Canvas";
|
||||||
import WebFont from "webfontloader";
|
import WebFont from "webfontloader";
|
||||||
|
|
@ -8,12 +8,12 @@ import SheetLeftPanel from "./components/Layouts/SheetLeftPanel";
|
||||||
import CanvasCapture from "./components/CanvasCapture";
|
import CanvasCapture from "./components/CanvasCapture";
|
||||||
import { Toaster } from "./components/ui/toaster";
|
import { Toaster } from "./components/ui/toaster";
|
||||||
import { Sidebar } from "./components/Layouts/LeftSidebar";
|
import { Sidebar } from "./components/Layouts/LeftSidebar";
|
||||||
import RightPanel from "./components/Layouts/RightPanel";
|
|
||||||
import { EditorPanel } from "./components/Panel/EditorPanel";
|
|
||||||
import { Canvas } from "./components/Panel/Canvas";
|
import { Canvas } from "./components/Panel/Canvas";
|
||||||
import TextPanel from "./components/Panel/TextPanel";
|
import TextPanel from "./components/Panel/TextPanel";
|
||||||
import { TopBar } from "./components/Panel/TopBar";
|
import { TopBar } from "./components/Panel/TopBar";
|
||||||
import { ActionButtons } from "./components/ActionButtons";
|
import { ActionButtons } from "./components/ActionButtons";
|
||||||
|
import EditorPanel from "./components/Panel/EditorPanel";
|
||||||
|
import CanvasContext from "./components/Context/canvasContext/CanvasContext";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -74,7 +74,7 @@ function App() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectedItem, setSelectedItem] = useState("text");
|
const { selectedPanel } = useContext(CanvasContext);
|
||||||
const [hasSelectedObject, setHasSelectedObject] = useState(true);
|
const [hasSelectedObject, setHasSelectedObject] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -93,8 +93,8 @@ function App() {
|
||||||
// </div>
|
// </div>
|
||||||
|
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Sidebar selectedItem={selectedItem} onItemSelect={setSelectedItem} />
|
<Sidebar />
|
||||||
{selectedItem === "text" && <TextPanel />}
|
{selectedPanel !== "" && <EditorPanel />}
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<TopBar isVisible={hasSelectedObject} />
|
<TopBar isVisible={hasSelectedObject} />
|
||||||
<ActionButtons />
|
<ActionButtons />
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,58 @@
|
||||||
import { ChevronDown } from "lucide-react";
|
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "./ui/select";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
const aspectRatios = [
|
||||||
|
{ value: "1:1", label: "Square (1:1)" },
|
||||||
|
{ value: "4:3", label: "Standard (4:3)" },
|
||||||
|
{ value: "3:2", label: "Classic (3:2)" },
|
||||||
|
{ value: "16:9", label: "Widescreen (16:9)" },
|
||||||
|
{ value: "9:16", label: "Portrait (9:16)" },
|
||||||
|
{ value: "21:9", label: "Ultrawide (21:9)" },
|
||||||
|
{ value: "32:9", label: "Super Ultrawide (32:9)" },
|
||||||
|
{ value: "1.85:1", label: "Cinema Standard (1.85:1)" },
|
||||||
|
{ value: "2.39:1", label: "Anamorphic Widescreen (2.39:1)" },
|
||||||
|
{ value: "2.76:1", label: "Ultra Panavision 70 (2.76:1)" },
|
||||||
|
{ value: "5:4", label: "Large Format (5:4)" },
|
||||||
|
{ value: "7:5", label: "Artistic Format (7:5)" },
|
||||||
|
{ value: "11:8.5", label: "Letter Size (11:8.5)" },
|
||||||
|
{ value: "3:4", label: "Portrait (3:4)" },
|
||||||
|
{ value: "1.91:1", label: "Facebook Ads (1.91:1)" },
|
||||||
|
];
|
||||||
|
|
||||||
export function ActionButtons() {
|
export function ActionButtons() {
|
||||||
|
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
|
||||||
|
const handleRatioChange = (newRatio) => {
|
||||||
|
setCanvasRatio(newRatio);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-4 right-0 z-50 flex items-center gap-2 bg-white rounded-l-[16px]">
|
<div className="absolute top-4 right-0 z-50 flex items-center gap-2 bg-white rounded-l-[16px]">
|
||||||
<div className="px-2 py-2">
|
<div className="px-2 py-2">
|
||||||
<Button variant="ghost" className="gap-2 h-9 px-2">
|
<div className="w-full">
|
||||||
<svg
|
<Select onValueChange={handleRatioChange} value={canvasRatio}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<SelectTrigger className="w-full text-xs font-bold">
|
||||||
width="12"
|
<SelectValue placeholder="Select aspect ratio" />
|
||||||
height="12"
|
</SelectTrigger>
|
||||||
viewBox="0 0 12 12"
|
<SelectContent>
|
||||||
fill="none"
|
{aspectRatios.map((ratio) => (
|
||||||
>
|
<SelectItem
|
||||||
<g clipPath="url(#clip0_71_2678)">
|
key={ratio.value}
|
||||||
<path d="M1 3.5L11 3.5" stroke="black" strokeLinecap="round" />
|
value={ratio.value}
|
||||||
<path
|
className="text-xs font-bold"
|
||||||
d="M1 8.5769L11 8.57691"
|
>
|
||||||
stroke="black"
|
{ratio.label}
|
||||||
strokeLinecap="round"
|
</SelectItem>
|
||||||
/>
|
))}
|
||||||
<path
|
</SelectContent>
|
||||||
d="M9.0769 1L9.0769 11"
|
</Select>
|
||||||
stroke="black"
|
</div>
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3.4231 1L3.4231 11"
|
|
||||||
stroke="black"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_71_2678">
|
|
||||||
<rect width="12" height="12" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
Resize <ChevronDown className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-5">
|
<div className="mr-5">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,38 @@
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from "react";
|
||||||
import CanvasContext from './CanvasContext';
|
import CanvasContext from "./CanvasContext";
|
||||||
|
|
||||||
const CanvasContextProvider = ({ children }) => {
|
const CanvasContextProvider = ({ children }) => {
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
const [canvas, setCanvas] = useState(null);
|
const [canvas, setCanvas] = useState(null);
|
||||||
const [canvasHeight, setCanvasHeight] = useState(0);
|
const [canvasHeight, setCanvasHeight] = useState(0);
|
||||||
const [canvasWidth, setCanvasWidth] = useState(0);
|
const [canvasWidth, setCanvasWidth] = useState(0);
|
||||||
const [screenWidth, setScreenWidth] = useState(0);
|
const [screenWidth, setScreenWidth] = useState(0);
|
||||||
const fabricCanvasRef = useRef(null);
|
const [canvasRatio, setCanvasRatio] = useState("4:3");
|
||||||
|
const [selectedPanel, setSelectedPanel] = useState("");
|
||||||
|
const fabricCanvasRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasContext.Provider value={{ canvasRef, canvas, setCanvas, fabricCanvasRef, canvasHeight, setCanvasHeight, canvasWidth, setCanvasWidth, screenWidth, setScreenWidth }}>
|
<CanvasContext.Provider
|
||||||
{children}
|
value={{
|
||||||
</CanvasContext.Provider>
|
canvasRef,
|
||||||
)
|
canvas,
|
||||||
}
|
setCanvas,
|
||||||
|
fabricCanvasRef,
|
||||||
|
canvasHeight,
|
||||||
|
canvasRatio,
|
||||||
|
setCanvasRatio,
|
||||||
|
selectedPanel,
|
||||||
|
setSelectedPanel,
|
||||||
|
setCanvasHeight,
|
||||||
|
canvasWidth,
|
||||||
|
setCanvasWidth,
|
||||||
|
screenWidth,
|
||||||
|
setScreenWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CanvasContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default CanvasContextProvider
|
export default CanvasContextProvider;
|
||||||
|
|
|
||||||
|
|
@ -1,183 +1,217 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { useContext, useRef, useState } from 'react';
|
import { useContext, useRef, useState } from "react";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from "fabric";
|
||||||
import Resizer from "react-image-file-resizer";
|
import Resizer from "react-image-file-resizer";
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
import { Slider } from '@/components/ui/slider';
|
Select,
|
||||||
import { HardDriveUpload, ImagePlus, Upload, Crop, RotateCw, FileType, ChevronUp, ChevronDown } from 'lucide-react';
|
SelectContent,
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
SelectItem,
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
SelectTrigger,
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
SelectValue,
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
} from "@/components/ui/select";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import {
|
||||||
|
HardDriveUpload,
|
||||||
|
ImagePlus,
|
||||||
|
Upload,
|
||||||
|
Crop,
|
||||||
|
RotateCw,
|
||||||
|
FileType,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{ icon: Upload, title: 'Auto-Resize', description: 'Images larger than 1080px are automatically resized' },
|
{
|
||||||
{ icon: Crop, title: 'Custom Dimensions', description: 'Set your preferred resize dimensions' },
|
icon: Upload,
|
||||||
{ icon: RotateCw, title: 'Quality & Rotation', description: 'Adjust image quality and rotation as needed' },
|
title: "Auto-Resize",
|
||||||
{ icon: FileType, title: 'Format Conversion', description: 'Convert between various image formats' },
|
description: "Images larger than 1080px are automatically resized",
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
icon: Crop,
|
||||||
|
title: "Custom Dimensions",
|
||||||
|
description: "Set your preferred resize dimensions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RotateCw,
|
||||||
|
title: "Quality & Rotation",
|
||||||
|
description: "Adjust image quality and rotation as needed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: FileType,
|
||||||
|
title: "Format Conversion",
|
||||||
|
description: "Convert between various image formats",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const AddImageIntoShape = () => {
|
const AddImageIntoShape = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [width, setWidth] = useState(1080);
|
const [width, setWidth] = useState(1080);
|
||||||
const [height, setHeight] = useState(1080);
|
const [height, setHeight] = useState(1080);
|
||||||
const [quality, setQuality] = useState(100);
|
const [quality, setQuality] = useState(100);
|
||||||
const [rotation, setRotation] = useState("0");
|
const [rotation, setRotation] = useState("0");
|
||||||
const [format, setFormat] = useState('JPEG');
|
const [format, setFormat] = useState("JPEG");
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const handleResize = (file, callback) => {
|
const handleResize = (file, callback) => {
|
||||||
Resizer.imageFileResizer(
|
Resizer.imageFileResizer(
|
||||||
file,
|
file,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
format,
|
format,
|
||||||
quality,
|
quality,
|
||||||
parseInt(rotation),
|
parseInt(rotation),
|
||||||
(resizedFile) => {
|
(resizedFile) => {
|
||||||
callback(resizedFile); // Pass the resized file to the callback
|
callback(resizedFile); // Pass the resized file to the callback
|
||||||
},
|
},
|
||||||
'file' // Output type
|
"file" // Output type
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileHandler = (e) => {
|
const fileHandler = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
setErrorMessage("No file selected.");
|
setErrorMessage("No file selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the file is an SVG
|
// Check if the file is an SVG
|
||||||
if (file.type === "image/svg+xml") {
|
if (file.type === "image/svg+xml") {
|
||||||
// Add SVG directly to canvas without compression
|
// Add SVG directly to canvas without compression
|
||||||
const blobUrl = URL.createObjectURL(file);
|
const blobUrl = URL.createObjectURL(file);
|
||||||
const imgElement = new Image();
|
const imgElement = new Image();
|
||||||
imgElement.src = blobUrl;
|
imgElement.src = blobUrl;
|
||||||
|
|
||||||
imgElement.onload = () => {
|
imgElement.onload = () => {
|
||||||
handleImageInsert(imgElement); // Insert the image without resizing
|
handleImageInsert(imgElement); // Insert the image without resizing
|
||||||
URL.revokeObjectURL(blobUrl);
|
URL.revokeObjectURL(blobUrl);
|
||||||
clearFileInput();
|
clearFileInput();
|
||||||
};
|
};
|
||||||
|
|
||||||
imgElement.onerror = () => {
|
imgElement.onerror = () => {
|
||||||
console.error("Failed to load SVG.");
|
console.error("Failed to load SVG.");
|
||||||
setErrorMessage("Failed to load SVG.");
|
setErrorMessage("Failed to load SVG.");
|
||||||
URL.revokeObjectURL(blobUrl);
|
URL.revokeObjectURL(blobUrl);
|
||||||
clearFileInput();
|
clearFileInput();
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle raster images (JPEG, PNG, etc.)
|
// Handle raster images (JPEG, PNG, etc.)
|
||||||
const imgElement = new Image();
|
const imgElement = new Image();
|
||||||
const blobUrl = URL.createObjectURL(file);
|
const blobUrl = URL.createObjectURL(file);
|
||||||
imgElement.src = blobUrl;
|
imgElement.src = blobUrl;
|
||||||
|
|
||||||
imgElement.onload = () => {
|
imgElement.onload = () => {
|
||||||
// If the width is greater than 1080px, compress the image
|
// If the width is greater than 1080px, compress the image
|
||||||
if (imgElement.width > 1080) {
|
if (imgElement.width > 1080) {
|
||||||
handleResize(file, (compressedFile) => {
|
handleResize(file, (compressedFile) => {
|
||||||
const compressedBlobUrl = URL.createObjectURL(compressedFile);
|
const compressedBlobUrl = URL.createObjectURL(compressedFile);
|
||||||
const compressedImg = new Image();
|
const compressedImg = new Image();
|
||||||
compressedImg.src = compressedBlobUrl;
|
compressedImg.src = compressedBlobUrl;
|
||||||
|
|
||||||
compressedImg.onload = () => {
|
compressedImg.onload = () => {
|
||||||
handleImageInsert(compressedImg); // Insert the resized image
|
handleImageInsert(compressedImg); // Insert the resized image
|
||||||
URL.revokeObjectURL(compressedBlobUrl); // Clean up
|
URL.revokeObjectURL(compressedBlobUrl); // Clean up
|
||||||
clearFileInput();
|
|
||||||
};
|
|
||||||
|
|
||||||
compressedImg.onerror = () => {
|
|
||||||
console.error("Failed to load compressed image.");
|
|
||||||
setErrorMessage("Failed to load compressed image.");
|
|
||||||
URL.revokeObjectURL(compressedBlobUrl);
|
|
||||||
clearFileInput();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handleImageInsert(imgElement); // Insert the original image if no resizing needed
|
|
||||||
clearFileInput();
|
|
||||||
}
|
|
||||||
URL.revokeObjectURL(blobUrl); // Clean up
|
|
||||||
clearFileInput();
|
clearFileInput();
|
||||||
};
|
};
|
||||||
|
|
||||||
imgElement.onerror = () => {
|
compressedImg.onerror = () => {
|
||||||
console.error("Failed to load image.");
|
console.error("Failed to load compressed image.");
|
||||||
setErrorMessage("Failed to load image.");
|
setErrorMessage("Failed to load compressed image.");
|
||||||
URL.revokeObjectURL(blobUrl);
|
URL.revokeObjectURL(compressedBlobUrl);
|
||||||
clearFileInput();
|
clearFileInput();
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleImageInsert(imgElement); // Insert the original image if no resizing needed
|
||||||
|
clearFileInput();
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(blobUrl); // Clean up
|
||||||
|
clearFileInput();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearFileInput = () => {
|
imgElement.onerror = () => {
|
||||||
if (fileInputRef.current) {
|
console.error("Failed to load image.");
|
||||||
fileInputRef.current.value = "";
|
setErrorMessage("Failed to load image.");
|
||||||
}
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
clearFileInput();
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const handleImageInsert = (img) => {
|
const clearFileInput = () => {
|
||||||
if (!activeObject) {
|
if (fileInputRef.current) {
|
||||||
setErrorMessage("No active object selected!");
|
fileInputRef.current.value = "";
|
||||||
return;
|
}
|
||||||
}
|
};
|
||||||
// Ensure absolute positioning for the clipPath
|
|
||||||
activeObject.set({
|
|
||||||
isClipPath: true, // Custom property
|
|
||||||
absolutePositioned: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate scale factors based on clip object size
|
const handleImageInsert = (img) => {
|
||||||
let scaleX = activeObject.width / img.width;
|
if (!activeObject) {
|
||||||
let scaleY = activeObject.height / img.height;
|
setErrorMessage("No active object selected!");
|
||||||
if (activeObject?.width < 100) {
|
return;
|
||||||
scaleX = 0.2;
|
}
|
||||||
}
|
// Ensure absolute positioning for the clipPath
|
||||||
|
activeObject.set({
|
||||||
|
isClipPath: true, // Custom property
|
||||||
|
absolutePositioned: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (activeObject.height < 100) {
|
// Calculate scale factors based on clip object size
|
||||||
scaleY = 0.2;
|
let scaleX = activeObject.width / img.width;
|
||||||
}
|
let scaleY = activeObject.height / img.height;
|
||||||
|
if (activeObject?.width < 100) {
|
||||||
|
scaleX = 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a fabric image object with scaling and clipPath
|
if (activeObject.height < 100) {
|
||||||
const fabricImage = new fabric.Image(img, {
|
scaleY = 0.2;
|
||||||
scaleX: scaleX,
|
}
|
||||||
scaleY: scaleY,
|
|
||||||
left: activeObject.left,
|
|
||||||
top: activeObject.top,
|
|
||||||
clipPath: activeObject, // Apply clipPath to the image
|
|
||||||
originX: activeObject.originX, // Match origin point
|
|
||||||
originY: activeObject.originY // Match origin point
|
|
||||||
});
|
|
||||||
|
|
||||||
// Adjust position based on the clipPath's transformations
|
// Create a fabric image object with scaling and clipPath
|
||||||
fabricImage.set({
|
const fabricImage = new fabric.Image(img, {
|
||||||
left: activeObject.left,
|
scaleX: scaleX,
|
||||||
top: activeObject.top,
|
scaleY: scaleY,
|
||||||
angle: activeObject.angle // Match rotation if any
|
left: activeObject.left,
|
||||||
});
|
top: activeObject.top,
|
||||||
|
clipPath: activeObject, // Apply clipPath to the image
|
||||||
|
originX: activeObject.originX, // Match origin point
|
||||||
|
originY: activeObject.originY, // Match origin point
|
||||||
|
});
|
||||||
|
|
||||||
canvas.add(fabricImage);
|
// Adjust position based on the clipPath's transformations
|
||||||
canvas.setActiveObject(fabricImage);
|
fabricImage.set({
|
||||||
setActiveObject(fabricImage);
|
left: activeObject.left,
|
||||||
canvas.renderAll();
|
top: activeObject.top,
|
||||||
};
|
angle: activeObject.angle, // Match rotation if any
|
||||||
|
});
|
||||||
|
|
||||||
// canvas.remove(activeObject);
|
canvas.add(fabricImage);
|
||||||
|
canvas.setActiveObject(fabricImage);
|
||||||
|
setActiveObject(fabricImage);
|
||||||
|
canvas.renderAll();
|
||||||
|
};
|
||||||
|
|
||||||
const content = () => {
|
// canvas.remove(activeObject);
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
|
|
||||||
{/* <Card className="my-2">
|
const content = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* <Card className="my-2">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">Image Insertion <ImagePlus className="h-5 w-5" /></AlertTitle>
|
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">Image Insertion <ImagePlus className="h-5 w-5" /></AlertTitle>
|
||||||
|
|
@ -205,149 +239,164 @@ const AddImageIntoShape = () => {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card> */}
|
</Card> */}
|
||||||
|
|
||||||
<Card className="my-2">
|
<Card className="my-2">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">
|
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">
|
||||||
Image Insertion <ImagePlus className="h-5 w-5" />
|
Image Insertion <ImagePlus className="h-5 w-5" />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription className="mt-1">
|
<AlertDescription className="mt-1">
|
||||||
<p className="mb-1">
|
<p className="mb-1">
|
||||||
Insert and customize images within shapes. Adjust image position and clipping after insertion.
|
Insert and customize images within shapes. Adjust image
|
||||||
</p>
|
position and clipping after insertion.
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
</p>
|
||||||
<CollapsibleTrigger asChild>
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<Button variant="outline" className="w-full justify-between mt-2">
|
<CollapsibleTrigger asChild>
|
||||||
<span>Key Features</span>
|
<Button
|
||||||
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
variant="outline"
|
||||||
</Button>
|
className="w-full justify-between mt-2"
|
||||||
</CollapsibleTrigger>
|
>
|
||||||
<CollapsibleContent>
|
<span>Key Features</span>
|
||||||
<div className="grid grid-cols-1 gap-2 mt-2">
|
{isOpen ? (
|
||||||
{features.map((feature, index) => (
|
<ChevronUp className="h-4 w-4" />
|
||||||
<div
|
) : (
|
||||||
key={index}
|
<ChevronDown className="h-4 w-4" />
|
||||||
className="flex items-start p-4 bg-white rounded-lg shadow-sm"
|
)}
|
||||||
>
|
|
||||||
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold mb-1">{feature.title}</h3>
|
|
||||||
<p className="text-sm text-gray-600">{feature.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-red-600 text-sm mt-2">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
<div className='flex flex-col w-[100%]'>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{/* Width Slider */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">Width: {width}px</Label>
|
|
||||||
<Slider
|
|
||||||
min={300}
|
|
||||||
value={[width]}
|
|
||||||
max={2000}
|
|
||||||
step={10}
|
|
||||||
onValueChange={(value) => setWidth(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Height Slider */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">Height: {height}px</Label>
|
|
||||||
<Slider
|
|
||||||
min={300}
|
|
||||||
value={[height]}
|
|
||||||
max={2000}
|
|
||||||
step={10}
|
|
||||||
onValueChange={(value) => setHeight(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quality Slider */}
|
|
||||||
{
|
|
||||||
format === "JPEG" &&
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">Quality: {quality}%</Label>
|
|
||||||
<Slider
|
|
||||||
value={[quality]}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
onValueChange={(value) => setQuality(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-1 items-center'>
|
|
||||||
{/* Rotation */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Rotation: {rotation}°</Label>
|
|
||||||
|
|
||||||
<Select value={rotation} onValueChange={(value) => setRotation(value)}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Select rotation in degree" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">0</SelectItem>
|
|
||||||
<SelectItem value="45">45</SelectItem>
|
|
||||||
<SelectItem value="90">90</SelectItem>
|
|
||||||
<SelectItem value="180">180</SelectItem>
|
|
||||||
<SelectItem value="270">270</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Format Dropdown */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Format</Label>
|
|
||||||
<Select value={format} onValueChange={(value) => setFormat(value)}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Select format" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="JPEG">JPEG</SelectItem>
|
|
||||||
<SelectItem value="PNG">PNG</SelectItem>
|
|
||||||
<SelectItem value="WEBP">WEBP</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button className="w-fit h-fit flex flex-wrap gap-1 relative py-4 my-4 mx-auto">
|
|
||||||
<HardDriveUpload className="cursor-pointer" />
|
|
||||||
Upload Image
|
|
||||||
<Input
|
|
||||||
className="cursor-pointer bg-white text-black absolute top-0 opacity-0"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={fileHandler} />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</CollapsibleTrigger>
|
||||||
</div>
|
<CollapsibleContent>
|
||||||
)
|
<div className="grid grid-cols-1 gap-2 mt-2">
|
||||||
}
|
{features.map((feature, index) => (
|
||||||
|
<div
|
||||||
return (
|
key={index}
|
||||||
<Card className="flex flex-col p-2">
|
className="flex items-start p-4 bg-white rounded-lg shadow-sm"
|
||||||
<CollapsibleComponent text={"Insert Image"}>
|
>
|
||||||
{content()}
|
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
||||||
</CollapsibleComponent>
|
<div>
|
||||||
|
<h3 className="font-semibold mb-1">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-red-600 text-sm mt-2">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col w-[100%]">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{/* Width Slider */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">Width: {width}px</Label>
|
||||||
|
<Slider
|
||||||
|
min={300}
|
||||||
|
value={[width]}
|
||||||
|
max={2000}
|
||||||
|
step={10}
|
||||||
|
onValueChange={(value) => setWidth(value[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Height Slider */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">Height: {height}px</Label>
|
||||||
|
<Slider
|
||||||
|
min={300}
|
||||||
|
value={[height]}
|
||||||
|
max={2000}
|
||||||
|
step={10}
|
||||||
|
onValueChange={(value) => setHeight(value[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quality Slider */}
|
||||||
|
{format === "JPEG" && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">Quality: {quality}%</Label>
|
||||||
|
<Slider
|
||||||
|
value={[quality]}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onValueChange={(value) => setQuality(value[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-1 items-center">
|
||||||
|
{/* Rotation */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Rotation: {rotation}°</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={rotation}
|
||||||
|
onValueChange={(value) => setRotation(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select rotation in degree" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">0</SelectItem>
|
||||||
|
<SelectItem value="45">45</SelectItem>
|
||||||
|
<SelectItem value="90">90</SelectItem>
|
||||||
|
<SelectItem value="180">180</SelectItem>
|
||||||
|
<SelectItem value="270">270</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Format Dropdown */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">Format</Label>
|
||||||
|
<Select
|
||||||
|
value={format}
|
||||||
|
onValueChange={(value) => setFormat(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select format" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="JPEG">JPEG</SelectItem>
|
||||||
|
<SelectItem value="PNG">PNG</SelectItem>
|
||||||
|
<SelectItem value="WEBP">WEBP</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="w-fit h-fit flex flex-wrap gap-1 relative py-4 my-4 mx-auto">
|
||||||
|
<HardDriveUpload className="cursor-pointer" />
|
||||||
|
Upload Image
|
||||||
|
<Input
|
||||||
|
className="cursor-pointer bg-white text-black absolute top-0 opacity-0"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={fileHandler}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="flex flex-col shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Insert Image"}>
|
||||||
|
{content()}
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddImageIntoShape;
|
export default AddImageIntoShape;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Button } from "@/components/ui/button";
|
import {
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
Collapsible,
|
||||||
import { ChevronUp, ChevronDown } from "lucide-react";
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const CollapsibleComponent = ({ children, text }) => {
|
const CollapsibleComponent = ({ children, text }) => {
|
||||||
|
|
@ -10,27 +12,25 @@ const CollapsibleComponent = ({ children, text }) => {
|
||||||
// Check if the text prop is "Canvas Setting" and set isOpen to false
|
// Check if the text prop is "Canvas Setting" and set isOpen to false
|
||||||
if (text === "Canvas Setting") {
|
if (text === "Canvas Setting") {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
} else {
|
||||||
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
else {
|
}, [text]);
|
||||||
setIsOpen(true)
|
|
||||||
}
|
|
||||||
}, [text])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<div className={`flex items-center justify-between cursor-pointer ${!isOpen ? "my-2" : ""}`}>
|
<div
|
||||||
<h2 className='font-bold'>{text}</h2>
|
className={`flex items-center justify-between cursor-pointer ${
|
||||||
<Button variant={"outline"}>
|
!isOpen ? "my-2" : ""
|
||||||
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
}`}
|
||||||
</Button>
|
>
|
||||||
|
<h2 className="font-bold mb-2">{text}</h2>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>{children}</CollapsibleContent>
|
||||||
{children}
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CollapsibleComponent
|
export default CollapsibleComponent;
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,73 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { Lock, Unlock } from "lucide-react";
|
import { Lock, Unlock } from "lucide-react";
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
const LockObject = () => {
|
const LockObject = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const [isLocked, setIsLocked] = useState(false);
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject?.lockMovementX === false) {
|
if (activeObject?.lockMovementX === false) {
|
||||||
setIsLocked(false);
|
setIsLocked(false);
|
||||||
}
|
} else {
|
||||||
else {
|
setIsLocked(true);
|
||||||
setIsLocked(true);
|
}
|
||||||
}
|
}, [activeObject]);
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
const toggleLock = () => {
|
const toggleLock = () => {
|
||||||
if (!canvas || !activeObject) {
|
if (!canvas || !activeObject) {
|
||||||
toast({
|
toast({
|
||||||
title: "No object selected",
|
title: "No object selected",
|
||||||
description: "Please select an object to lock or unlock.",
|
description: "Please select an object to lock or unlock.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
});
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const newLockState = !activeObject.lockMovementX;
|
|
||||||
activeObject.set({
|
|
||||||
lockMovementX: newLockState,
|
|
||||||
lockMovementY: newLockState,
|
|
||||||
lockRotation: newLockState,
|
|
||||||
lockScalingX: newLockState,
|
|
||||||
lockScalingY: newLockState,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsLocked(newLockState);
|
|
||||||
canvas.requestRenderAll();
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: newLockState ? "Object locked" : "Object unlocked",
|
|
||||||
description: newLockState ? "The object is now locked in place." : "The object can now be moved and resized.",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const newLockState = !activeObject.lockMovementX;
|
||||||
<Card className="p-2">
|
activeObject.set({
|
||||||
<h2 className='font-bold'>{!isLocked ? "Lock" : "Unlock"} Object</h2>
|
lockMovementX: newLockState,
|
||||||
<Button
|
lockMovementY: newLockState,
|
||||||
onClick={toggleLock}
|
lockRotation: newLockState,
|
||||||
variant="outline"
|
lockScalingX: newLockState,
|
||||||
size="icon"
|
lockScalingY: newLockState,
|
||||||
disabled={!activeObject}
|
});
|
||||||
title={isLocked ? "Unlock object" : "Lock object"}
|
|
||||||
>
|
|
||||||
{isLocked ? <Unlock className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LockObject
|
setIsLocked(newLockState);
|
||||||
|
canvas.requestRenderAll();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: newLockState ? "Object locked" : "Object unlocked",
|
||||||
|
description: newLockState
|
||||||
|
? "The object is now locked in place."
|
||||||
|
: "The object can now be moved and resized.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="shadow-none border-0">
|
||||||
|
<h2 className="font-bold">{!isLocked ? "Lock" : "Unlock"} Object</h2>
|
||||||
|
<Button
|
||||||
|
onClick={toggleLock}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
disabled={!activeObject}
|
||||||
|
title={isLocked ? "Unlock object" : "Lock object"}
|
||||||
|
>
|
||||||
|
{isLocked ? (
|
||||||
|
<Unlock className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Lock className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LockObject;
|
||||||
|
|
|
||||||
|
|
@ -1,162 +1,158 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, ChevronUp, ChevronDown } from 'lucide-react';
|
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
|
||||||
|
|
||||||
const PositionCustomization = () => {
|
const PositionCustomization = () => {
|
||||||
const { canvas } = useContext(CanvasContext)
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { activeObject } = useContext(ActiveObjectContext)
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
const [objectPosition, setObjectPosition] = useState({ left: 50, top: 50 })
|
const [objectPosition, setObjectPosition] = useState({ left: 50, top: 50 });
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
useEffect(() => {
|
||||||
|
if (activeObject) {
|
||||||
useEffect(() => {
|
setObjectPosition({
|
||||||
if (activeObject) {
|
left: Math.round(activeObject.left || 0),
|
||||||
setObjectPosition({
|
top: Math.round(activeObject.top || 0),
|
||||||
left: Math.round(activeObject.left || 0),
|
});
|
||||||
top: Math.round(activeObject.top || 0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
const updateObjectPosition = (key, value) => {
|
|
||||||
const updatedPosition = { ...objectPosition, [key]: value }
|
|
||||||
setObjectPosition(updatedPosition)
|
|
||||||
|
|
||||||
if (canvas && activeObject) {
|
|
||||||
activeObject.set(updatedPosition)
|
|
||||||
canvas.renderAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
const handleInputChange = (key, value) => {
|
const updateObjectPosition = (key, value) => {
|
||||||
const numValue = parseInt(value, 10)
|
const updatedPosition = { ...objectPosition, [key]: value };
|
||||||
if (!isNaN(numValue)) {
|
setObjectPosition(updatedPosition);
|
||||||
updateObjectPosition(key, numValue)
|
|
||||||
}
|
if (canvas && activeObject) {
|
||||||
|
activeObject.set(updatedPosition);
|
||||||
|
canvas.renderAll();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSliderChange = (key, value) => {
|
const handleInputChange = (key, value) => {
|
||||||
updateObjectPosition(key, value[0])
|
const numValue = parseInt(value, 10);
|
||||||
|
if (!isNaN(numValue)) {
|
||||||
|
updateObjectPosition(key, numValue);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const adjustPosition = (key, delta) => {
|
const handleSliderChange = (key, value) => {
|
||||||
const newValue = objectPosition[key] + delta
|
updateObjectPosition(key, value[0]);
|
||||||
updateObjectPosition(key, newValue)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const content = () => {
|
const adjustPosition = (key, delta) => {
|
||||||
return (
|
const newValue = objectPosition[key] + delta;
|
||||||
<CardContent className="space-y-6 p-0">
|
updateObjectPosition(key, newValue);
|
||||||
<div className="space-y-4">
|
};
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="position-left">Left</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Input
|
|
||||||
id="position-left"
|
|
||||||
type="number"
|
|
||||||
value={objectPosition.left}
|
|
||||||
onChange={(e) => handleInputChange('left', e.target.value)}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
min={0}
|
|
||||||
max={canvas ? canvas.width : 1000}
|
|
||||||
step={1}
|
|
||||||
value={[objectPosition.left]}
|
|
||||||
onValueChange={(value) => handleSliderChange('left', value)}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="position-top">Top</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Input
|
|
||||||
id="position-top"
|
|
||||||
type="number"
|
|
||||||
value={objectPosition.top}
|
|
||||||
onChange={(e) => handleInputChange('top', e.target.value)}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
<Slider
|
|
||||||
min={0}
|
|
||||||
max={canvas ? canvas.height : 1000}
|
|
||||||
step={1}
|
|
||||||
value={[objectPosition.top]}
|
|
||||||
onValueChange={(value) => handleSliderChange('top', value)}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-32 h-32 grid grid-cols-3 gap-1 p-2 mx-auto">
|
|
||||||
<div className="col-start-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => adjustPosition('top', -1)}
|
|
||||||
aria-label="Move up"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="w-full h-full"
|
|
||||||
>
|
|
||||||
<ArrowUp className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="col-start-1 row-start-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => adjustPosition('left', -1)}
|
|
||||||
aria-label="Move left"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="w-full h-full"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="col-start-3 row-start-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => adjustPosition('left', 1)}
|
|
||||||
aria-label="Move right"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="w-full h-full"
|
|
||||||
>
|
|
||||||
<ArrowRight className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="col-start-2 row-start-3">
|
|
||||||
<Button
|
|
||||||
onClick={() => adjustPosition('top', 1)}
|
|
||||||
aria-label="Move down"
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="w-full h-full"
|
|
||||||
>
|
|
||||||
<ArrowDown className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</CardContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<CardContent className="space-y-6 p-0">
|
||||||
<CollapsibleComponent text={"Position Control"}>
|
<div className="space-y-4">
|
||||||
{content()}
|
<div className="space-y-1">
|
||||||
</CollapsibleComponent>
|
<Label htmlFor="position-left">Left</Label>
|
||||||
</Card>
|
<div className="flex items-center space-x-2">
|
||||||
)
|
<Input
|
||||||
}
|
id="position-left"
|
||||||
|
type="number"
|
||||||
|
value={objectPosition.left}
|
||||||
|
onChange={(e) => handleInputChange("left", e.target.value)}
|
||||||
|
className="w-20"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
min={0}
|
||||||
|
max={canvas ? canvas.width : 1000}
|
||||||
|
step={1}
|
||||||
|
value={[objectPosition.left]}
|
||||||
|
onValueChange={(value) => handleSliderChange("left", value)}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="position-top">Top</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Input
|
||||||
|
id="position-top"
|
||||||
|
type="number"
|
||||||
|
value={objectPosition.top}
|
||||||
|
onChange={(e) => handleInputChange("top", e.target.value)}
|
||||||
|
className="w-20"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
min={0}
|
||||||
|
max={canvas ? canvas.height : 1000}
|
||||||
|
step={1}
|
||||||
|
value={[objectPosition.top]}
|
||||||
|
onValueChange={(value) => handleSliderChange("top", value)}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
export default PositionCustomization
|
<div className="w-32 h-32 grid grid-cols-3 gap-1 p-2 mx-auto">
|
||||||
|
<div className="col-start-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => adjustPosition("top", -1)}
|
||||||
|
aria-label="Move up"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<ArrowUp className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="col-start-1 row-start-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => adjustPosition("left", -1)}
|
||||||
|
aria-label="Move left"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="col-start-3 row-start-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => adjustPosition("left", 1)}
|
||||||
|
aria-label="Move right"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="col-start-2 row-start-3">
|
||||||
|
<Button
|
||||||
|
onClick={() => adjustPosition("top", 1)}
|
||||||
|
aria-label="Move down"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<ArrowDown className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Position Control"}>
|
||||||
|
{content()}
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PositionCustomization;
|
||||||
|
|
|
||||||
|
|
@ -6,89 +6,89 @@ import { useContext, useEffect, useState } from "react";
|
||||||
import CollapsibleComponent from "./CollapsibleComponent";
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const ScaleObjects = () => {
|
const ScaleObjects = () => {
|
||||||
const { canvas } = useContext(CanvasContext); // Access Fabric.js canvas from context
|
const { canvas } = useContext(CanvasContext); // Access Fabric.js canvas from context
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const [scaleX, setScaleX] = useState(1);
|
const [scaleX, setScaleX] = useState(1);
|
||||||
const [scaleY, setScaleY] = useState(1);
|
const [scaleY, setScaleY] = useState(1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject) {
|
if (activeObject) {
|
||||||
setScaleX(activeObject?.scaleX);
|
setScaleX(activeObject?.scaleX);
|
||||||
setScaleY(activeObject?.scaleY);
|
setScaleY(activeObject?.scaleY);
|
||||||
}
|
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
// Handle scaleX changes
|
|
||||||
const handleScaleXChange = (value) => {
|
|
||||||
const newScaleX = Math.max(0.001, Math.min(value)); // Clamp scaleX between 0.001 and 3
|
|
||||||
setScaleX(newScaleX);
|
|
||||||
if (canvas && activeObject) {
|
|
||||||
activeObject.scaleX = newScaleX;
|
|
||||||
canvas.discardActiveObject();
|
|
||||||
canvas.setActiveObject(activeObject);
|
|
||||||
canvas.renderAll(); // Re-render the canvas
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle scaleY changes
|
|
||||||
const handleScaleYChange = (value) => {
|
|
||||||
const newScaleY = Math.max(0.001, Math.min(value)); // Clamp scaleY between 0.001 and 3
|
|
||||||
setScaleY(newScaleY);
|
|
||||||
if (canvas && activeObject) {
|
|
||||||
activeObject.scaleY = newScaleY;
|
|
||||||
canvas.discardActiveObject();
|
|
||||||
canvas.setActiveObject(activeObject);
|
|
||||||
canvas.renderAll(); // Re-render the canvas
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const content = () => {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-2 items-center gap-1">
|
|
||||||
{/* Scale X Input */}
|
|
||||||
<div className="flex flex-col space-y-1">
|
|
||||||
<label className="text-xs font-medium">X: {scaleX.toFixed(3)}</label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.010"
|
|
||||||
min="0.001"
|
|
||||||
value={scaleX}
|
|
||||||
onChange={(e) => {
|
|
||||||
const inputValue = parseFloat(e.target.value);
|
|
||||||
if (!isNaN(inputValue)) {
|
|
||||||
handleScaleXChange(inputValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Scale Y Input */}
|
|
||||||
<div className="flex flex-col space-y-1">
|
|
||||||
<label className="text-xs font-medium">Y: {scaleY.toFixed(3)}</label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
step="0.010"
|
|
||||||
min="0.001"
|
|
||||||
value={scaleY}
|
|
||||||
onChange={(e) => {
|
|
||||||
const inputValue = parseFloat(e.target.value);
|
|
||||||
if (!isNaN(inputValue)) {
|
|
||||||
handleScaleYChange(inputValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
|
// Handle scaleX changes
|
||||||
|
const handleScaleXChange = (value) => {
|
||||||
|
const newScaleX = Math.max(0.001, Math.min(value)); // Clamp scaleX between 0.001 and 3
|
||||||
|
setScaleX(newScaleX);
|
||||||
|
if (canvas && activeObject) {
|
||||||
|
activeObject.scaleX = newScaleX;
|
||||||
|
canvas.discardActiveObject();
|
||||||
|
canvas.setActiveObject(activeObject);
|
||||||
|
canvas.renderAll(); // Re-render the canvas
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle scaleY changes
|
||||||
|
const handleScaleYChange = (value) => {
|
||||||
|
const newScaleY = Math.max(0.001, Math.min(value)); // Clamp scaleY between 0.001 and 3
|
||||||
|
setScaleY(newScaleY);
|
||||||
|
if (canvas && activeObject) {
|
||||||
|
activeObject.scaleY = newScaleY;
|
||||||
|
canvas.discardActiveObject();
|
||||||
|
canvas.setActiveObject(activeObject);
|
||||||
|
canvas.renderAll(); // Re-render the canvas
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
<Card className="grid items-center gap-2 p-2">
|
<div className="grid grid-cols-2 items-center gap-1">
|
||||||
<CollapsibleComponent text={"Scale Control"}>
|
{/* Scale X Input */}
|
||||||
{content()}
|
<div className="flex flex-col space-y-1">
|
||||||
</CollapsibleComponent>
|
<label className="text-xs font-medium">X: {scaleX.toFixed(3)}</label>
|
||||||
</Card>
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.010"
|
||||||
|
min="0.001"
|
||||||
|
value={scaleX}
|
||||||
|
onChange={(e) => {
|
||||||
|
const inputValue = parseFloat(e.target.value);
|
||||||
|
if (!isNaN(inputValue)) {
|
||||||
|
handleScaleXChange(inputValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scale Y Input */}
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<label className="text-xs font-medium">Y: {scaleY.toFixed(3)}</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.010"
|
||||||
|
min="0.001"
|
||||||
|
value={scaleY}
|
||||||
|
onChange={(e) => {
|
||||||
|
const inputValue = parseFloat(e.target.value);
|
||||||
|
if (!isNaN(inputValue)) {
|
||||||
|
handleScaleYChange(inputValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="grid items-center gap-2 shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Scale Control"}>
|
||||||
|
{content()}
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScaleObjects;
|
export default ScaleObjects;
|
||||||
|
|
|
||||||
|
|
@ -1,151 +1,159 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
import { useContext, useEffect, useRef, useState } from 'react'
|
Select,
|
||||||
import { Card, } from '@/components/ui/card';
|
SelectContent,
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
SelectItem,
|
||||||
import { Separator } from '@/components/ui/separator';
|
SelectTrigger,
|
||||||
import { fabric } from 'fabric';
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { fabric } from "fabric";
|
||||||
|
|
||||||
const SelectObjectFromGroup = () => {
|
const SelectObjectFromGroup = () => {
|
||||||
const [groupObjects, setGroupObjects] = useState([])
|
const [groupObjects, setGroupObjects] = useState([]);
|
||||||
const [selectedObject, setSelectedObject] = useState(null)
|
const [selectedObject, setSelectedObject] = useState(null);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext)
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
const { canvas } = useContext(CanvasContext)
|
const { canvas } = useContext(CanvasContext);
|
||||||
const previewCanvasRef = useRef(null)
|
const previewCanvasRef = useRef(null);
|
||||||
|
|
||||||
const activeObject = canvas?.getActiveObject()
|
const activeObject = canvas?.getActiveObject();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject?.type === 'group') {
|
if (activeObject?.type === "group") {
|
||||||
setGroupObjects(activeObject._objects || [])
|
setGroupObjects(activeObject._objects || []);
|
||||||
setSelectedObject(null)
|
setSelectedObject(null);
|
||||||
} else {
|
} else {
|
||||||
setGroupObjects([])
|
setGroupObjects([]);
|
||||||
setSelectedObject(null)
|
setSelectedObject(null);
|
||||||
}
|
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (selectedObject && previewCanvasRef.current) {
|
|
||||||
// const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
|
|
||||||
|
|
||||||
// previewCanvas.setDimensions({
|
|
||||||
// width: 100,
|
|
||||||
// height: 100
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // Clone the active object to create a true deep copy
|
|
||||||
// selectedObject.clone((clonedObject) => {
|
|
||||||
// if (selectedObject?.width > 100 || selectedObject?.height > 100) {
|
|
||||||
// clonedObject.set({
|
|
||||||
// scaleX: 0.2,
|
|
||||||
// scaleY: 0.2,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// // Add the cloned object to the canvas
|
|
||||||
// clonedObject.set({
|
|
||||||
// left: 50,
|
|
||||||
// top: 50,
|
|
||||||
// originX: 'center',
|
|
||||||
// originY: 'center',
|
|
||||||
// selectable: false,
|
|
||||||
// evented: false,
|
|
||||||
// })
|
|
||||||
// previewCanvas.add(clonedObject);
|
|
||||||
// previewCanvas.renderAll();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// previewCanvas.dispose()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, [selectedObject])
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedObject && previewCanvasRef.current) {
|
|
||||||
// Create a new static canvas
|
|
||||||
const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
|
|
||||||
|
|
||||||
previewCanvas.setDimensions({
|
|
||||||
width: 100,
|
|
||||||
height: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear previous objects (if any)
|
|
||||||
previewCanvas.clear();
|
|
||||||
|
|
||||||
// Clone the active object for preview
|
|
||||||
selectedObject.clone((clonedObject) => {
|
|
||||||
const scaledWidth = selectedObject.width * selectedObject.scaleX;
|
|
||||||
const scaledHeight = selectedObject.height * selectedObject.scaleY;
|
|
||||||
|
|
||||||
if (scaledWidth > 100 || scaledHeight > 100) {
|
|
||||||
clonedObject.scaleToWidth(100); // Scale to fit width
|
|
||||||
clonedObject.scaleToHeight(100); // Scale to fit height
|
|
||||||
}
|
|
||||||
|
|
||||||
clonedObject.set({
|
|
||||||
left: 50,
|
|
||||||
top: 50,
|
|
||||||
originX: 'center',
|
|
||||||
originY: 'center',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the cloned object to preview canvas
|
|
||||||
previewCanvas.add(clonedObject);
|
|
||||||
previewCanvas.renderAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup function to dispose of the canvas
|
|
||||||
return () => {
|
|
||||||
previewCanvas.clear(); // Clear all objects
|
|
||||||
previewCanvas.dispose(); // Properly dispose the canvas
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [selectedObject, previewCanvasRef]);
|
|
||||||
|
|
||||||
|
|
||||||
const handleSelectObject = (value) => {
|
|
||||||
const selected = groupObjects[parseInt(value)]
|
|
||||||
setSelectedObject(selected)
|
|
||||||
setActiveObject(selected)
|
|
||||||
}
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
if (!activeObject || activeObject.type !== 'group') {
|
// useEffect(() => {
|
||||||
return null
|
// if (selectedObject && previewCanvasRef.current) {
|
||||||
|
// const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
|
||||||
|
|
||||||
|
// previewCanvas.setDimensions({
|
||||||
|
// width: 100,
|
||||||
|
// height: 100
|
||||||
|
// })
|
||||||
|
|
||||||
|
// // Clone the active object to create a true deep copy
|
||||||
|
// selectedObject.clone((clonedObject) => {
|
||||||
|
// if (selectedObject?.width > 100 || selectedObject?.height > 100) {
|
||||||
|
// clonedObject.set({
|
||||||
|
// scaleX: 0.2,
|
||||||
|
// scaleY: 0.2,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// // Add the cloned object to the canvas
|
||||||
|
// clonedObject.set({
|
||||||
|
// left: 50,
|
||||||
|
// top: 50,
|
||||||
|
// originX: 'center',
|
||||||
|
// originY: 'center',
|
||||||
|
// selectable: false,
|
||||||
|
// evented: false,
|
||||||
|
// })
|
||||||
|
// previewCanvas.add(clonedObject);
|
||||||
|
// previewCanvas.renderAll();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// previewCanvas.dispose()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }, [selectedObject])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedObject && previewCanvasRef.current) {
|
||||||
|
// Create a new static canvas
|
||||||
|
const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
|
||||||
|
|
||||||
|
previewCanvas.setDimensions({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear previous objects (if any)
|
||||||
|
previewCanvas.clear();
|
||||||
|
|
||||||
|
// Clone the active object for preview
|
||||||
|
selectedObject.clone((clonedObject) => {
|
||||||
|
const scaledWidth = selectedObject.width * selectedObject.scaleX;
|
||||||
|
const scaledHeight = selectedObject.height * selectedObject.scaleY;
|
||||||
|
|
||||||
|
if (scaledWidth > 100 || scaledHeight > 100) {
|
||||||
|
clonedObject.scaleToWidth(100); // Scale to fit width
|
||||||
|
clonedObject.scaleToHeight(100); // Scale to fit height
|
||||||
|
}
|
||||||
|
|
||||||
|
clonedObject.set({
|
||||||
|
left: 50,
|
||||||
|
top: 50,
|
||||||
|
originX: "center",
|
||||||
|
originY: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the cloned object to preview canvas
|
||||||
|
previewCanvas.add(clonedObject);
|
||||||
|
previewCanvas.renderAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup function to dispose of the canvas
|
||||||
|
return () => {
|
||||||
|
previewCanvas.clear(); // Clear all objects
|
||||||
|
previewCanvas.dispose(); // Properly dispose the canvas
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
}, [selectedObject, previewCanvasRef]);
|
||||||
|
|
||||||
return (
|
const handleSelectObject = (value) => {
|
||||||
<div>
|
const selected = groupObjects[parseInt(value)];
|
||||||
<Card className="p-4">
|
setSelectedObject(selected);
|
||||||
<h2 className='font-bold mb-4'>Group Objects</h2>
|
setActiveObject(selected);
|
||||||
<div className='flex flex-col items-center justify-center space-y-4'>
|
};
|
||||||
<Select
|
|
||||||
onValueChange={handleSelectObject}
|
|
||||||
value={selectedObject ? groupObjects.indexOf(selectedObject).toString() : undefined}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full max-w-xs">
|
|
||||||
<SelectValue placeholder="Select object" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="max-h-[250px]">
|
|
||||||
{groupObjects.map((object, index) => (
|
|
||||||
<SelectItem key={index} value={index.toString()}>
|
|
||||||
{object.name || `Object ${index + 1}`}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{selectedObject && (
|
if (!activeObject || activeObject.type !== "group") {
|
||||||
<div className='w-[100px] h-[100px] bg-muted rounded-md overflow-hidden border border-gray-300'>
|
return null;
|
||||||
<canvas ref={previewCanvasRef} width={100} height={100} />
|
}
|
||||||
</div>
|
|
||||||
)}
|
return (
|
||||||
</div>
|
<div>
|
||||||
</Card>
|
<Card className="p-4 shadow-none border-0">
|
||||||
<Separator className="my-4" />
|
<h2 className="font-bold mb-4">Group Objects</h2>
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-4">
|
||||||
|
<Select
|
||||||
|
onValueChange={handleSelectObject}
|
||||||
|
value={
|
||||||
|
selectedObject
|
||||||
|
? groupObjects.indexOf(selectedObject).toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full max-w-xs">
|
||||||
|
<SelectValue placeholder="Select object" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-[250px]">
|
||||||
|
{groupObjects.map((object, index) => (
|
||||||
|
<SelectItem key={index} value={index.toString()}>
|
||||||
|
{object.name || `Object ${index + 1}`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{selectedObject && (
|
||||||
|
<div className="w-[100px] h-[100px] bg-muted rounded-md overflow-hidden border border-gray-300">
|
||||||
|
<canvas ref={previewCanvasRef} width={100} height={100} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
</Card>
|
||||||
}
|
<Separator className="my-4" />
|
||||||
export default SelectObjectFromGroup;
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SelectObjectFromGroup;
|
||||||
|
|
|
||||||
|
|
@ -9,135 +9,143 @@ import { useContext, useEffect, useState } from "react";
|
||||||
import CollapsibleComponent from "./CollapsibleComponent";
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const ShadowCustomization = () => {
|
const ShadowCustomization = () => {
|
||||||
// get values from context
|
// get values from context
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
||||||
// state to handle shadow inputs
|
// state to handle shadow inputs
|
||||||
const [shadowColor, setShadowColor] = useState("");
|
const [shadowColor, setShadowColor] = useState("");
|
||||||
const [offsetX, setOffsetX] = useState(5);
|
const [offsetX, setOffsetX] = useState(5);
|
||||||
const [offsetY, setOffsetY] = useState(5);
|
const [offsetY, setOffsetY] = useState(5);
|
||||||
const [blur, setBlur] = useState(0.5);
|
const [blur, setBlur] = useState(0.5);
|
||||||
const [opacity, setOpacity] = useState(0.5);
|
const [opacity, setOpacity] = useState(0.5);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject) {
|
if (activeObject) {
|
||||||
setShadowColor(activeObject?.shadow?.color || "#db1d62");
|
setShadowColor(activeObject?.shadow?.color || "#db1d62");
|
||||||
setOffsetX(activeObject?.shadow?.offsetX || 5);
|
setOffsetX(activeObject?.shadow?.offsetX || 5);
|
||||||
setOffsetY(activeObject?.shadow?.offsetY || 5);
|
setOffsetY(activeObject?.shadow?.offsetY || 5);
|
||||||
setBlur(activeObject?.shadow?.blur || 0.5);
|
setBlur(activeObject?.shadow?.blur || 0.5);
|
||||||
setOpacity(activeObject?.shadow?.opacity || 0.5);
|
setOpacity(activeObject?.shadow?.opacity || 0.5);
|
||||||
}
|
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
const handleApplyShadow = () => {
|
|
||||||
if (activeObject && canvas) {
|
|
||||||
const shadow = {
|
|
||||||
color: shadowColor,
|
|
||||||
blur: blur,
|
|
||||||
offsetX: offsetX,
|
|
||||||
offsetY: offsetY,
|
|
||||||
opacity: opacity,
|
|
||||||
};
|
|
||||||
activeObject.set("shadow", shadow);
|
|
||||||
// Ensure object updates and renders
|
|
||||||
activeObject.dirty = true; // Mark the object as dirty for re-render
|
|
||||||
canvas.renderAll(); // Trigger canvas re-render
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
const handleDisableShadow = () => {
|
const handleApplyShadow = () => {
|
||||||
if (activeObject && canvas) {
|
if (activeObject && canvas) {
|
||||||
activeObject.set("shadow", null)
|
const shadow = {
|
||||||
canvas.renderAll()
|
color: shadowColor,
|
||||||
}
|
blur: blur,
|
||||||
|
offsetX: offsetX,
|
||||||
|
offsetY: offsetY,
|
||||||
|
opacity: opacity,
|
||||||
|
};
|
||||||
|
activeObject.set("shadow", shadow);
|
||||||
|
// Ensure object updates and renders
|
||||||
|
activeObject.dirty = true; // Mark the object as dirty for re-render
|
||||||
|
canvas.renderAll(); // Trigger canvas re-render
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const content = () => {
|
const handleDisableShadow = () => {
|
||||||
return (
|
if (activeObject && canvas) {
|
||||||
<div>
|
activeObject.set("shadow", null);
|
||||||
<div className="space-y-2">
|
canvas.renderAll();
|
||||||
<div>
|
|
||||||
<Label htmlFor="shadowColor">Shadow Color</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Input
|
|
||||||
id="shadowColor"
|
|
||||||
type="color"
|
|
||||||
value={shadowColor}
|
|
||||||
onChange={(e) => setShadowColor(e.target.value)}
|
|
||||||
className="w-16 h-10"
|
|
||||||
/>
|
|
||||||
<span>{shadowColor}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Offset X: {offsetX}px</Label>
|
|
||||||
<Slider
|
|
||||||
value={[offsetX]}
|
|
||||||
onValueChange={(value) => setOffsetX(value[0])}
|
|
||||||
max={50}
|
|
||||||
min={-50}
|
|
||||||
step={1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Offset Y: {offsetY}px</Label>
|
|
||||||
<Slider
|
|
||||||
value={[offsetY]}
|
|
||||||
onValueChange={(value) => setOffsetY(value[0])}
|
|
||||||
max={50}
|
|
||||||
min={-50}
|
|
||||||
step={1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Blur: {blur}px</Label>
|
|
||||||
<Slider
|
|
||||||
value={[blur]}
|
|
||||||
onValueChange={(value) => setBlur(value[0])}
|
|
||||||
max={50}
|
|
||||||
min={0}
|
|
||||||
step={1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Opacity: {opacity}</Label>
|
|
||||||
<Slider
|
|
||||||
value={[opacity]}
|
|
||||||
onValueChange={(value) => setOpacity(value[0])}
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
step={0.1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-32 h-32 bg-white rounded-md flex items-center justify-center mx-auto border-2 my-4" style={{
|
|
||||||
boxShadow: `${offsetX}px ${offsetY}px ${blur}px ${shadowColor}${Math.round(opacity * 255).toString(16).padStart(2, '0')}`
|
|
||||||
}}>
|
|
||||||
<span className="text-4xl">A</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Button onClick={handleApplyShadow}>Apply Shadow</Button>
|
|
||||||
<Button variant="outline" onClick={handleDisableShadow}>Disable Shadow</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
<Card className='p-2'>
|
<div>
|
||||||
<CollapsibleComponent text={"Shadow Control"}>
|
<div className="space-y-2">
|
||||||
{content()}
|
<div>
|
||||||
</CollapsibleComponent>
|
<Label htmlFor="shadowColor">Shadow Color</Label>
|
||||||
</Card>
|
<div className="flex items-center space-x-2">
|
||||||
|
<Input
|
||||||
|
id="shadowColor"
|
||||||
|
type="color"
|
||||||
|
value={shadowColor}
|
||||||
|
onChange={(e) => setShadowColor(e.target.value)}
|
||||||
|
className="w-16 h-10"
|
||||||
|
/>
|
||||||
|
<span>{shadowColor}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Offset X: {offsetX}px</Label>
|
||||||
|
<Slider
|
||||||
|
value={[offsetX]}
|
||||||
|
onValueChange={(value) => setOffsetX(value[0])}
|
||||||
|
max={50}
|
||||||
|
min={-50}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Offset Y: {offsetY}px</Label>
|
||||||
|
<Slider
|
||||||
|
value={[offsetY]}
|
||||||
|
onValueChange={(value) => setOffsetY(value[0])}
|
||||||
|
max={50}
|
||||||
|
min={-50}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Blur: {blur}px</Label>
|
||||||
|
<Slider
|
||||||
|
value={[blur]}
|
||||||
|
onValueChange={(value) => setBlur(value[0])}
|
||||||
|
max={50}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Opacity: {opacity}</Label>
|
||||||
|
<Slider
|
||||||
|
value={[opacity]}
|
||||||
|
onValueChange={(value) => setOpacity(value[0])}
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="w-32 h-32 bg-white rounded-md flex items-center justify-center mx-auto border-2 my-4"
|
||||||
|
style={{
|
||||||
|
boxShadow: `${offsetX}px ${offsetY}px ${blur}px ${shadowColor}${Math.round(
|
||||||
|
opacity * 255
|
||||||
|
)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-4xl">A</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Button onClick={handleApplyShadow}>Apply Shadow</Button>
|
||||||
|
<Button variant="outline" onClick={handleDisableShadow}>
|
||||||
|
Disable Shadow
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Shadow Control"}>
|
||||||
|
{content()}
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShadowCustomization;
|
export default ShadowCustomization;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,85 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from "@/components/ui/card";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useContext, useEffect, useState } from 'react'
|
import { useContext, useEffect, useState } from "react";
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const SkewCustomization = () => {
|
const SkewCustomization = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const [skewX, setSkewX] = useState(0);
|
const [skewX, setSkewX] = useState(0);
|
||||||
const [skewY, setSkewY] = useState(0);
|
const [skewY, setSkewY] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject) {
|
if (activeObject) {
|
||||||
setSkewX(activeObject?.skewX);
|
setSkewX(activeObject?.skewX);
|
||||||
setSkewY(activeObject?.skewY);
|
setSkewY(activeObject?.skewY);
|
||||||
}
|
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
// Update skewX directly
|
|
||||||
const handleSkewXChange = (value) => {
|
|
||||||
setSkewX(value[0]);
|
|
||||||
if (activeObject && canvas) {
|
|
||||||
activeObject.set({ skewX: value[0] });
|
|
||||||
canvas.discardActiveObject();
|
|
||||||
canvas.setActiveObject(activeObject);
|
|
||||||
canvas.renderAll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update skewY directly
|
|
||||||
const handleSkewYChange = (value) => {
|
|
||||||
setSkewY(value[0]);
|
|
||||||
if (activeObject && canvas) {
|
|
||||||
activeObject.set({ skewY: value[0] });
|
|
||||||
canvas.discardActiveObject();
|
|
||||||
canvas.setActiveObject(activeObject);
|
|
||||||
canvas.renderAll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const content = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Label className="mb-2">X: {skewX}°</Label>
|
|
||||||
<Slider
|
|
||||||
min={-90}
|
|
||||||
max={90}
|
|
||||||
step={1}
|
|
||||||
value={[skewX]}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleSkewXChange(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<Label className="mb-2">Y: {skewY}°</Label>
|
|
||||||
<Slider
|
|
||||||
min={-90}
|
|
||||||
max={90}
|
|
||||||
step={1}
|
|
||||||
value={[skewY]}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleSkewYChange(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
|
// Update skewX directly
|
||||||
|
const handleSkewXChange = (value) => {
|
||||||
|
setSkewX(value[0]);
|
||||||
|
if (activeObject && canvas) {
|
||||||
|
activeObject.set({ skewX: value[0] });
|
||||||
|
canvas.discardActiveObject();
|
||||||
|
canvas.setActiveObject(activeObject);
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update skewY directly
|
||||||
|
const handleSkewYChange = (value) => {
|
||||||
|
setSkewY(value[0]);
|
||||||
|
if (activeObject && canvas) {
|
||||||
|
activeObject.set({ skewY: value[0] });
|
||||||
|
canvas.discardActiveObject();
|
||||||
|
canvas.setActiveObject(activeObject);
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<div>
|
||||||
<CollapsibleComponent text={"Skew Control"}>
|
<div className="w-full">
|
||||||
{content()}
|
<Label className="mb-2">X: {skewX}°</Label>
|
||||||
</CollapsibleComponent>
|
<Slider
|
||||||
</Card>
|
min={-90}
|
||||||
)
|
max={90}
|
||||||
}
|
step={1}
|
||||||
|
value={[skewX]}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
handleSkewXChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
export default SkewCustomization
|
<div className="w-full">
|
||||||
|
<Label className="mb-2">Y: {skewY}°</Label>
|
||||||
|
<Slider
|
||||||
|
min={-90}
|
||||||
|
max={90}
|
||||||
|
step={1}
|
||||||
|
value={[skewY]}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
handleSkewYChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Skew Control"}>
|
||||||
|
{content()}
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SkewCustomization;
|
||||||
|
|
|
||||||
|
|
@ -1,367 +1,397 @@
|
||||||
import { useContext, useState, useRef, useEffect } from 'react'
|
import { useContext, useState, useRef, useEffect } from "react";
|
||||||
import { fabric } from 'fabric'
|
import { fabric } from "fabric";
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext'
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import {
|
||||||
import { Slider } from '@/components/ui/slider'
|
Select,
|
||||||
import { Button } from '@/components/ui/button'
|
SelectContent,
|
||||||
import { Paintbrush, ContrastIcon as Gradient } from 'lucide-react'
|
SelectItem,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
SelectTrigger,
|
||||||
import CollapsibleComponent from './CollapsibleComponent'
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Paintbrush } from "lucide-react";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const StrokeCustomization = () => {
|
const StrokeCustomization = () => {
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const previewRef = useRef(null);
|
const previewRef = useRef(null);
|
||||||
|
|
||||||
const [strokeWidth, setStrokeWidth] = useState(0);
|
const [strokeWidth, setStrokeWidth] = useState(0);
|
||||||
const [strokeColor, setStrokeColor] = useState("#000000");
|
const [strokeColor, setStrokeColor] = useState("#000000");
|
||||||
const [gradientStrokeColors, setGradientStrokeColors] = useState({
|
const [gradientStrokeColors, setGradientStrokeColors] = useState({
|
||||||
color1: "#f97316",
|
color1: "#f97316",
|
||||||
color2: "#e26286",
|
color2: "#e26286",
|
||||||
});
|
});
|
||||||
const [colorType, setColorType] = useState("color");
|
const [colorType, setColorType] = useState("color");
|
||||||
const [gradientDirection, setGradientDirection] = useState("to bottom");
|
const [gradientDirection, setGradientDirection] = useState("to bottom");
|
||||||
|
|
||||||
|
// Utility function to handle styles of objects
|
||||||
// Utility function to handle styles of objects
|
const handleObjectStyle = (object) => {
|
||||||
const handleObjectStyle = (object) => {
|
if (object.stroke) {
|
||||||
// Determine fill type (solid or gradient)
|
if (typeof object.stroke === "string") {
|
||||||
if (object.stroke) {
|
setColorType("color");
|
||||||
if (typeof object.stroke === "string") {
|
setStrokeColor(object.stroke); // Solid color fill
|
||||||
setColorType("color");
|
} else if (object.stroke.colorStops) {
|
||||||
setStrokeColor(object.stroke); // Solid color fill
|
setColorType("gradient");
|
||||||
} else if (object.stroke.colorStops) {
|
setGradientStrokeColors({
|
||||||
setColorType("gradient");
|
color1: object.stroke.colorStops[0]?.color || "#f97316",
|
||||||
setGradientStrokeColors({
|
color2: object.stroke.colorStops[1]?.color || "#e26286",
|
||||||
color1: object.stroke.colorStops[0]?.color || "#f97316",
|
|
||||||
color2: object.stroke.colorStops[1]?.color || "#e26286",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle stroke width
|
|
||||||
if (object.strokeWidth) {
|
|
||||||
setStrokeWidth(object.strokeWidth || 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Recursively process group objects
|
|
||||||
const processGroupObjects = (group) => {
|
|
||||||
group._objects.forEach((obj) => {
|
|
||||||
if (obj.type === "group") {
|
|
||||||
processGroupObjects(obj); // Handle nested groups
|
|
||||||
} else {
|
|
||||||
handleObjectStyle(obj); // Apply styles to each object
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (object.strokeWidth) {
|
||||||
|
setStrokeWidth(object.strokeWidth || 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursively process group objects
|
||||||
|
const processGroupObjects = (group) => {
|
||||||
|
group._objects.forEach((obj) => {
|
||||||
|
if (obj.type === "group") {
|
||||||
|
processGroupObjects(obj); // Handle nested groups
|
||||||
|
} else {
|
||||||
|
handleObjectStyle(obj); // Apply styles to each object
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Effect to get previous values from active object
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeObject) {
|
||||||
|
if (activeObject.type === "group") {
|
||||||
|
processGroupObjects(activeObject); // Process grouped objects
|
||||||
|
} else {
|
||||||
|
handleObjectStyle(activeObject); // Process single object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
|
const updatePreview = () => {
|
||||||
|
if (!previewRef.current) return;
|
||||||
|
|
||||||
|
const previewStyle = {
|
||||||
|
width: "80px",
|
||||||
|
height: "80px",
|
||||||
|
border: `${strokeWidth}px solid`,
|
||||||
|
borderRadius: "4px",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Effect to get previous values from active object
|
if (colorType === "color") {
|
||||||
useEffect(() => {
|
previewStyle.borderColor = strokeColor;
|
||||||
if (activeObject) {
|
} else {
|
||||||
if (activeObject.type === "group") {
|
previewStyle.borderImage = `linear-gradient(${gradientDirection}, ${gradientStrokeColors.color1}, ${gradientStrokeColors.color2}) 1`;
|
||||||
processGroupObjects(activeObject); // Process grouped objects
|
|
||||||
} else {
|
|
||||||
handleObjectStyle(activeObject); // Process single object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [activeObject]);
|
|
||||||
|
|
||||||
const updatePreview = () => {
|
|
||||||
if (!previewRef.current) return;
|
|
||||||
|
|
||||||
const previewStyle = {
|
|
||||||
width: '80px',
|
|
||||||
height: '80px',
|
|
||||||
border: `${strokeWidth}px solid`,
|
|
||||||
borderRadius: '4px',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (colorType === "color") {
|
|
||||||
previewStyle.borderColor = strokeColor;
|
|
||||||
} else {
|
|
||||||
previewStyle.borderImage = `linear-gradient(${gradientDirection}, ${gradientStrokeColors.color1}, ${gradientStrokeColors.color2}) 1`;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(previewRef.current.style, previewStyle);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updatePreview();
|
|
||||||
}, [strokeWidth, strokeColor, gradientStrokeColors, colorType, gradientDirection]);
|
|
||||||
|
|
||||||
const handleStrokeWidthChange = (value) => {
|
|
||||||
setStrokeWidth(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleColorTypeChange = (type) => {
|
|
||||||
setColorType(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStrokeColorChange = (e) => {
|
|
||||||
setStrokeColor(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGradientColorChange = (key, e) => {
|
|
||||||
setGradientStrokeColors(prev => ({ ...prev, [key]: e.target.value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyStrokeStyle = () => {
|
|
||||||
if (!activeObject || activeObject.type === "line") return;
|
|
||||||
|
|
||||||
const width = activeObject?.width || 0;
|
|
||||||
const height = activeObject?.height || 0;
|
|
||||||
|
|
||||||
const coords = {
|
|
||||||
"to bottom": { x1: 0, y1: 0, x2: 0, y2: height },
|
|
||||||
"to top": { x1: 0, y1: height, x2: 0, y2: 0 },
|
|
||||||
"to right": { x1: 0, y1: 0, x2: width, y2: 0 },
|
|
||||||
"to left": { x1: width, y1: 0, x2: 0, y2: 0 },
|
|
||||||
};
|
|
||||||
const directionCoords = coords[gradientDirection];
|
|
||||||
|
|
||||||
const applyStrokeStyle = (object) => {
|
|
||||||
object.set("strokeWidth", strokeWidth);
|
|
||||||
|
|
||||||
if (colorType === "color") {
|
|
||||||
object.set("stroke", strokeColor);
|
|
||||||
} else if (colorType === "gradient") {
|
|
||||||
const gradient = new fabric.Gradient({
|
|
||||||
type: "linear",
|
|
||||||
gradientUnits: "pixels",
|
|
||||||
coords: directionCoords,
|
|
||||||
colorStops: [
|
|
||||||
{ offset: 0, color: gradientStrokeColors.color1 },
|
|
||||||
{ offset: 1, color: gradientStrokeColors.color2 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
object.set("stroke", gradient);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const processGroupObjects = (group) => {
|
|
||||||
group._objects.forEach((obj) => {
|
|
||||||
if (obj.type === "group") {
|
|
||||||
processGroupObjects(obj);
|
|
||||||
} else {
|
|
||||||
applyStrokeStyle(obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (activeObject.type === "group") {
|
|
||||||
processGroupObjects(activeObject);
|
|
||||||
} else {
|
|
||||||
applyStrokeStyle(activeObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.renderAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
const revertStroke = () => {
|
|
||||||
if (!activeObject) return;
|
|
||||||
|
|
||||||
const revertObject = (object) => {
|
|
||||||
object.set("strokeWidth", 0);
|
|
||||||
object.set("stroke", null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const processGroupObjects = (group) => {
|
|
||||||
group._objects.forEach((obj) => {
|
|
||||||
if (obj.type === "group") {
|
|
||||||
processGroupObjects(obj);
|
|
||||||
} else {
|
|
||||||
revertObject(obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (activeObject.type === "group") {
|
|
||||||
processGroupObjects(activeObject);
|
|
||||||
} else {
|
|
||||||
revertObject(activeObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.renderAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
const content = () => {
|
|
||||||
return (
|
|
||||||
<CardContent className="p-0 mb-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="stroke-width">Stroke Width</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Slider
|
|
||||||
id="stroke-width"
|
|
||||||
min={0}
|
|
||||||
max={50}
|
|
||||||
step={1}
|
|
||||||
value={[strokeWidth]}
|
|
||||||
onValueChange={([value]) => handleStrokeWidthChange(value)}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={50}
|
|
||||||
value={strokeWidth}
|
|
||||||
onChange={(e) => handleStrokeWidthChange(Number(e.target.value))}
|
|
||||||
className="w-16"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Tabs value={colorType} onValueChange={(value) => handleColorTypeChange(value)}>
|
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
|
||||||
<TabsTrigger value="color">
|
|
||||||
<Paintbrush className="w-4 h-4 mr-2" />
|
|
||||||
Solid
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="gradient">
|
|
||||||
<Gradient className="w-4 h-4 mr-2" />
|
|
||||||
Gradient
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="color" className="space-y-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="stroke-color">Stroke Color</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="stroke-color"
|
|
||||||
type="color"
|
|
||||||
value={strokeColor}
|
|
||||||
onChange={handleStrokeColorChange}
|
|
||||||
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
style={{ backgroundColor: strokeColor, borderRadius: '0.375rem' }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={strokeColor}
|
|
||||||
onChange={handleStrokeColorChange}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="gradient" className="space-y-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="gradient-direction">Gradient Direction</Label>
|
|
||||||
<Select value={gradientDirection} onValueChange={setGradientDirection}>
|
|
||||||
<SelectTrigger id="gradient-direction">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="to bottom">Top to Bottom</SelectItem>
|
|
||||||
<SelectItem value="to top">Bottom to Top</SelectItem>
|
|
||||||
<SelectItem value="to right">Left to Right</SelectItem>
|
|
||||||
<SelectItem value="to left">Right to Left</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="gradient-color-1">Gradient Color 1</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="gradient-color-1"
|
|
||||||
type="color"
|
|
||||||
value={gradientStrokeColors.color1}
|
|
||||||
onChange={(e) => handleGradientColorChange('color1', e)}
|
|
||||||
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
style={{ backgroundColor: gradientStrokeColors.color1, borderRadius: '0.375rem' }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={gradientStrokeColors.color1}
|
|
||||||
onChange={(e) => handleGradientColorChange('color1', e)}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="gradient-color-2">Gradient Color 2</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="gradient-color-2"
|
|
||||||
type="color"
|
|
||||||
value={gradientStrokeColors.color2}
|
|
||||||
onChange={(e) => handleGradientColorChange('color2', e)}
|
|
||||||
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
style={{ backgroundColor: gradientStrokeColors.color2, borderRadius: '0.375rem' }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={gradientStrokeColors.color2}
|
|
||||||
onChange={(e) => handleGradientColorChange('color2', e)}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label>Preview</Label>
|
|
||||||
<div className="border rounded-md p-2 flex items-center justify-center" style={{ height: '120px' }}>
|
|
||||||
<div ref={previewRef} style={{ width: '80px', height: '80px' }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Button onClick={applyStrokeStyle}>
|
|
||||||
Apply Stroke
|
|
||||||
</Button>
|
|
||||||
<Button onClick={revertStroke} variant="outline">
|
|
||||||
Revert Stroke
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.assign(previewRef.current.style, previewStyle);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updatePreview();
|
||||||
|
}, [
|
||||||
|
strokeWidth,
|
||||||
|
strokeColor,
|
||||||
|
gradientStrokeColors,
|
||||||
|
colorType,
|
||||||
|
gradientDirection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleStrokeWidthChange = (value) => {
|
||||||
|
setStrokeWidth(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColorTypeChange = (type) => {
|
||||||
|
setColorType(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStrokeColorChange = (e) => {
|
||||||
|
setStrokeColor(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGradientColorChange = (key, e) => {
|
||||||
|
setGradientStrokeColors((prev) => ({ ...prev, [key]: e.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyStrokeStyle = () => {
|
||||||
|
if (!activeObject || activeObject.type === "line") return;
|
||||||
|
|
||||||
|
const width = activeObject?.width || 0;
|
||||||
|
const height = activeObject?.height || 0;
|
||||||
|
|
||||||
|
const coords = {
|
||||||
|
"to bottom": { x1: 0, y1: 0, x2: 0, y2: height },
|
||||||
|
"to top": { x1: 0, y1: height, x2: 0, y2: 0 },
|
||||||
|
"to right": { x1: 0, y1: 0, x2: width, y2: 0 },
|
||||||
|
"to left": { x1: width, y1: 0, x2: 0, y2: 0 },
|
||||||
|
};
|
||||||
|
const directionCoords = coords[gradientDirection];
|
||||||
|
|
||||||
|
const applyStrokeStyle = (object) => {
|
||||||
|
object.set("strokeWidth", strokeWidth);
|
||||||
|
|
||||||
|
if (colorType === "color") {
|
||||||
|
object.set("stroke", strokeColor);
|
||||||
|
} else if (colorType === "gradient") {
|
||||||
|
const gradient = new fabric.Gradient({
|
||||||
|
type: "linear",
|
||||||
|
gradientUnits: "pixels",
|
||||||
|
coords: directionCoords,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: gradientStrokeColors.color1 },
|
||||||
|
{ offset: 1, color: gradientStrokeColors.color2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
object.set("stroke", gradient);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processGroupObjects = (group) => {
|
||||||
|
group._objects.forEach((obj) => {
|
||||||
|
if (obj.type === "group") {
|
||||||
|
processGroupObjects(obj);
|
||||||
|
} else {
|
||||||
|
applyStrokeStyle(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeObject.type === "group") {
|
||||||
|
processGroupObjects(activeObject);
|
||||||
|
} else {
|
||||||
|
applyStrokeStyle(activeObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.renderAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Automatically apply stroke styles on state change
|
||||||
|
useEffect(() => {
|
||||||
|
applyStrokeStyle();
|
||||||
|
}, [
|
||||||
|
strokeWidth,
|
||||||
|
strokeColor,
|
||||||
|
gradientStrokeColors,
|
||||||
|
colorType,
|
||||||
|
gradientDirection,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const revertStroke = () => {
|
||||||
|
if (!activeObject) return;
|
||||||
|
|
||||||
|
const revertObject = (object) => {
|
||||||
|
object.set("strokeWidth", 0);
|
||||||
|
object.set("stroke", null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processGroupObjects = (group) => {
|
||||||
|
group._objects.forEach((obj) => {
|
||||||
|
if (obj.type === "group") {
|
||||||
|
processGroupObjects(obj);
|
||||||
|
} else {
|
||||||
|
revertObject(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeObject.type === "group") {
|
||||||
|
processGroupObjects(activeObject);
|
||||||
|
} else {
|
||||||
|
revertObject(activeObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.renderAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<CardContent className="p-0 mb-2">
|
||||||
<CollapsibleComponent text={"Stroke Control"}>
|
<div className="space-y-2">
|
||||||
{content()}
|
<div>
|
||||||
</CollapsibleComponent>
|
<Label htmlFor="stroke-width">Stroke Width</Label>
|
||||||
</Card>
|
<div className="flex items-center space-x-2">
|
||||||
|
<Slider
|
||||||
|
id="stroke-width"
|
||||||
|
min={0}
|
||||||
|
max={50}
|
||||||
|
step={1}
|
||||||
|
value={[strokeWidth]}
|
||||||
|
onValueChange={([value]) => handleStrokeWidthChange(value)}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={50}
|
||||||
|
value={strokeWidth}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleStrokeWidthChange(Number(e.target.value))
|
||||||
|
}
|
||||||
|
className="w-16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Tabs
|
||||||
|
value={colorType}
|
||||||
|
onValueChange={(value) => handleColorTypeChange(value)}
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="color">
|
||||||
|
<Paintbrush className="w-4 h-4 mr-2" />
|
||||||
|
Solid
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="gradient" className="flex gap-2">
|
||||||
|
<div className="h-4 w-4 rounded bg-gradient-to-r from-purple-500 to-pink-500" />
|
||||||
|
Gradient
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="color" className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="stroke-color">Stroke Color</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="stroke-color"
|
||||||
|
type="color"
|
||||||
|
value={strokeColor}
|
||||||
|
onChange={handleStrokeColorChange}
|
||||||
|
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: strokeColor,
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={strokeColor}
|
||||||
|
onChange={handleStrokeColorChange}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="gradient" className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="gradient-direction">Gradient Direction</Label>
|
||||||
|
<Select
|
||||||
|
value={gradientDirection}
|
||||||
|
onValueChange={setGradientDirection}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="gradient-direction">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="to bottom">Top to Bottom</SelectItem>
|
||||||
|
<SelectItem value="to top">Bottom to Top</SelectItem>
|
||||||
|
<SelectItem value="to right">Left to Right</SelectItem>
|
||||||
|
<SelectItem value="to left">Right to Left</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="gradient-color-1">Gradient Color 1</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="gradient-color-1"
|
||||||
|
type="color"
|
||||||
|
value={gradientStrokeColors.color1}
|
||||||
|
onChange={(e) => handleGradientColorChange("color1", e)}
|
||||||
|
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: gradientStrokeColors.color1,
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={gradientStrokeColors.color1}
|
||||||
|
onChange={(e) => handleGradientColorChange("color1", e)}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="gradient-color-2">Gradient Color 2</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="gradient-color-2"
|
||||||
|
type="color"
|
||||||
|
value={gradientStrokeColors.color2}
|
||||||
|
onChange={(e) => handleGradientColorChange("color2", e)}
|
||||||
|
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: gradientStrokeColors.color2,
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={gradientStrokeColors.color2}
|
||||||
|
onChange={(e) => handleGradientColorChange("color2", e)}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Preview</Label>
|
||||||
|
<div
|
||||||
|
className="border rounded-md p-2 flex items-center justify-center"
|
||||||
|
style={{ height: "120px" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={previewRef}
|
||||||
|
style={{ width: "80px", height: "80px" }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Button onClick={applyStrokeStyle} className="bg-[#FF2B85]">
|
||||||
|
Apply Stroke
|
||||||
|
</Button>
|
||||||
|
<Button onClick={revertStroke} variant="outline">
|
||||||
|
Revert Stroke
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className=" shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Stroke Control"}>
|
||||||
|
{content()}
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StrokeCustomization;
|
export default StrokeCustomization;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,314 +1,382 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
import { Slider } from '@/components/ui/slider';
|
Select,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
SelectContent,
|
||||||
import { useContext, useEffect, useState } from 'react'
|
SelectGroup,
|
||||||
import { AlignLeft, AlignCenter, AlignRight, Bold, Italic, Underline, Strikethrough } from 'lucide-react';
|
SelectItem,
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
AlignLeft,
|
||||||
|
AlignCenter,
|
||||||
|
AlignRight,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Underline,
|
||||||
|
Strikethrough,
|
||||||
|
} from "lucide-react";
|
||||||
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const fonts = [
|
const fonts = [
|
||||||
'Roboto', 'Open Sans', 'Lato', 'Montserrat', 'Raleway', 'Poppins', 'Merriweather',
|
"Roboto",
|
||||||
'Playfair Display', 'Nunito', 'Oswald', 'Source Sans Pro', 'Ubuntu', 'Noto Sans',
|
"Open Sans",
|
||||||
'Work Sans', 'Bebas Neue', 'Arimo', 'PT Sans', 'PT Serif', 'Titillium Web',
|
"Lato",
|
||||||
'Fira Sans', 'Karla', 'Josefin Sans', 'Cairo', 'Rubik', 'Mulish', 'IBM Plex Sans',
|
"Montserrat",
|
||||||
'Quicksand', 'Cabin', 'Heebo', 'Exo 2', 'Manrope', 'Jost', 'Anton', 'Asap',
|
"Raleway",
|
||||||
'Baloo 2', 'Barlow', 'Cantarell', 'Chivo', 'Inter', 'Dosis', 'Crimson Text',
|
"Poppins",
|
||||||
'Amatic SC', 'ABeeZee', 'Raleway Dots', 'Pacifico', 'Orbitron', 'Varela Round',
|
"Merriweather",
|
||||||
'Acme', 'Teko',
|
"Playfair Display",
|
||||||
|
"Nunito",
|
||||||
|
"Oswald",
|
||||||
|
"Source Sans Pro",
|
||||||
|
"Ubuntu",
|
||||||
|
"Noto Sans",
|
||||||
|
"Work Sans",
|
||||||
|
"Bebas Neue",
|
||||||
|
"Arimo",
|
||||||
|
"PT Sans",
|
||||||
|
"PT Serif",
|
||||||
|
"Titillium Web",
|
||||||
|
"Fira Sans",
|
||||||
|
"Karla",
|
||||||
|
"Josefin Sans",
|
||||||
|
"Cairo",
|
||||||
|
"Rubik",
|
||||||
|
"Mulish",
|
||||||
|
"IBM Plex Sans",
|
||||||
|
"Quicksand",
|
||||||
|
"Cabin",
|
||||||
|
"Heebo",
|
||||||
|
"Exo 2",
|
||||||
|
"Manrope",
|
||||||
|
"Jost",
|
||||||
|
"Anton",
|
||||||
|
"Asap",
|
||||||
|
"Baloo 2",
|
||||||
|
"Barlow",
|
||||||
|
"Cantarell",
|
||||||
|
"Chivo",
|
||||||
|
"Inter",
|
||||||
|
"Dosis",
|
||||||
|
"Crimson Text",
|
||||||
|
"Amatic SC",
|
||||||
|
"ABeeZee",
|
||||||
|
"Raleway Dots",
|
||||||
|
"Pacifico",
|
||||||
|
"Orbitron",
|
||||||
|
"Varela Round",
|
||||||
|
"Acme",
|
||||||
|
"Teko",
|
||||||
];
|
];
|
||||||
|
|
||||||
const TextCustomization = () => {
|
const TextCustomization = () => {
|
||||||
// get values from context
|
// get values from context
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState("");
|
||||||
const [fontFamily, setFontFamily] = useState('Arial');
|
const [fontFamily, setFontFamily] = useState("Arial");
|
||||||
const [fontSize, setFontSize] = useState(20);
|
const [fontSize, setFontSize] = useState(20);
|
||||||
const [fontStyle, setFontStyle] = useState('normal');
|
const [fontStyle, setFontStyle] = useState("normal");
|
||||||
const [fontWeight, setFontWeight] = useState('normal');
|
const [fontWeight, setFontWeight] = useState("normal");
|
||||||
const [lineHeight, setLineHeight] = useState(1.16);
|
const [lineHeight, setLineHeight] = useState(1.16);
|
||||||
const [charSpacing, setCharSpacing] = useState(0);
|
const [charSpacing, setCharSpacing] = useState(0);
|
||||||
const [underline, setUnderline] = useState(false);
|
const [underline, setUnderline] = useState(false);
|
||||||
const [linethrough, setLinethrough] = useState(false);
|
const [linethrough, setLinethrough] = useState(false);
|
||||||
const [textAlign, setTextAlign] = useState('left');
|
const [textAlign, setTextAlign] = useState("left");
|
||||||
const [previewText, setPreviewText] = useState('');
|
const [previewText, setPreviewText] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject?.type === "i-text") {
|
if (activeObject?.type === "i-text") {
|
||||||
setText(activeObject?.text || '');
|
setText(activeObject?.text || "");
|
||||||
setFontFamily(activeObject?.fontFamily || 'Arial');
|
setFontFamily(activeObject?.fontFamily || "Arial");
|
||||||
setFontSize(activeObject?.fontSize || 20);
|
setFontSize(activeObject?.fontSize || 20);
|
||||||
setFontStyle(activeObject?.fontStyle || 'normal');
|
setFontStyle(activeObject?.fontStyle || "normal");
|
||||||
setFontWeight(activeObject?.fontWeight || 'normal');
|
setFontWeight(activeObject?.fontWeight || "normal");
|
||||||
setLineHeight(activeObject?.lineHeight || 1.16);
|
setLineHeight(activeObject?.lineHeight || 1.16);
|
||||||
setCharSpacing(activeObject?.charSpacing || 0);
|
setCharSpacing(activeObject?.charSpacing || 0);
|
||||||
setUnderline(activeObject?.underline || false);
|
setUnderline(activeObject?.underline || false);
|
||||||
setLinethrough(activeObject?.linethrough || false);
|
setLinethrough(activeObject?.linethrough || false);
|
||||||
setTextAlign(activeObject?.textAlign || 'left');
|
setTextAlign(activeObject?.textAlign || "left");
|
||||||
setPreviewText(activeObject?.text || '');
|
setPreviewText(activeObject?.text || "");
|
||||||
}
|
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
const updateActiveObject = (properties) => {
|
|
||||||
if (activeObject?.type === "i-text") {
|
|
||||||
activeObject.set(properties)
|
|
||||||
canvas?.renderAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
const applyChanges = () => {
|
const updateActiveObject = (properties) => {
|
||||||
updateActiveObject({
|
if (activeObject?.type === "i-text") {
|
||||||
text,
|
activeObject.set(properties);
|
||||||
fontFamily,
|
canvas?.renderAll();
|
||||||
fontSize,
|
|
||||||
fontStyle,
|
|
||||||
fontWeight,
|
|
||||||
lineHeight,
|
|
||||||
charSpacing,
|
|
||||||
underline,
|
|
||||||
linethrough,
|
|
||||||
textAlign
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleTextChange = (newText) => {
|
const applyChanges = () => {
|
||||||
setText(newText);
|
updateActiveObject({
|
||||||
setPreviewText(newText);
|
text,
|
||||||
}
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontStyle,
|
||||||
|
fontWeight,
|
||||||
|
lineHeight,
|
||||||
|
charSpacing,
|
||||||
|
underline,
|
||||||
|
linethrough,
|
||||||
|
textAlign,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleFontFamilyChange = (newFontFamily) => {
|
const handleTextChange = (newText) => {
|
||||||
setFontFamily(newFontFamily);
|
setText(newText);
|
||||||
}
|
setPreviewText(newText);
|
||||||
|
};
|
||||||
|
|
||||||
const handleFontSizeChange = (newFontSize) => {
|
const handleFontFamilyChange = (newFontFamily) => {
|
||||||
setFontSize(newFontSize)
|
setFontFamily(newFontFamily);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleTextAlignChange = (newTextAlign) => {
|
const handleFontSizeChange = (newFontSize) => {
|
||||||
setTextAlign(newTextAlign)
|
setFontSize(newFontSize);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFontStyleChange = () => {
|
const handleTextAlignChange = (newTextAlign) => {
|
||||||
const newFontStyle = fontStyle === 'normal' ? 'italic' : 'normal'
|
setTextAlign(newTextAlign);
|
||||||
setFontStyle(newFontStyle)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleFontWeightChange = () => {
|
const handleFontStyleChange = () => {
|
||||||
const newFontWeight = fontWeight === 'normal' ? 'bold' : 'normal'
|
const newFontStyle = fontStyle === "normal" ? "italic" : "normal";
|
||||||
setFontWeight(newFontWeight)
|
setFontStyle(newFontStyle);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleLineHeightChange = (newLineHeight) => {
|
const handleFontWeightChange = () => {
|
||||||
setLineHeight(newLineHeight)
|
const newFontWeight = fontWeight === "normal" ? "bold" : "normal";
|
||||||
}
|
setFontWeight(newFontWeight);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCharSpacingChange = (newCharSpacing) => {
|
const handleLineHeightChange = (newLineHeight) => {
|
||||||
setCharSpacing(newCharSpacing)
|
setLineHeight(newLineHeight);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleUnderlineChange = () => {
|
const handleCharSpacingChange = (newCharSpacing) => {
|
||||||
const newUnderline = !underline
|
setCharSpacing(newCharSpacing);
|
||||||
setUnderline(newUnderline)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleLinethroughChange = () => {
|
const handleUnderlineChange = () => {
|
||||||
const newLinethrough = !linethrough
|
const newUnderline = !underline;
|
||||||
setLinethrough(newLinethrough)
|
setUnderline(newUnderline);
|
||||||
}
|
};
|
||||||
|
|
||||||
const content = () => {
|
const handleLinethroughChange = () => {
|
||||||
if (!(activeObject?.type === "i-text")) {
|
const newLinethrough = !linethrough;
|
||||||
return (
|
setLinethrough(newLinethrough);
|
||||||
<div className='mt-2'>
|
};
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
const content = () => {
|
||||||
<p className="text-center text-gray-500">Select a text object to customize</p>
|
if (!(activeObject?.type === "i-text")) {
|
||||||
</CardContent>
|
return (
|
||||||
</Card>
|
<div className="mt-2">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<p className="text-center text-gray-500">
|
||||||
|
Select a text object to customize
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<CardContent className="p-2">
|
||||||
|
<Tabs defaultValue="text" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="text">Text</TabsTrigger>
|
||||||
|
<TabsTrigger value="style">Style</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="text" className="space-y-1">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="text-content">Text Content</Label>
|
||||||
|
<Input
|
||||||
|
id="text-content"
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => handleTextChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Font Family</Label>
|
||||||
|
<Select
|
||||||
|
value={fontFamily}
|
||||||
|
onValueChange={handleFontFamilyChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a font" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="h-[250px]">
|
||||||
|
<SelectGroup>
|
||||||
|
{fonts.map((font) => (
|
||||||
|
<SelectItem
|
||||||
|
style={{ fontFamily: font }}
|
||||||
|
key={font}
|
||||||
|
value={font}
|
||||||
|
>
|
||||||
|
{font}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Font Size</Label>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Slider
|
||||||
|
value={[fontSize]}
|
||||||
|
onValueChange={([value]) => handleFontSizeChange(value)}
|
||||||
|
min={8}
|
||||||
|
max={72}
|
||||||
|
step={1}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={fontSize}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFontSizeChange(parseInt(e.target.value, 10) || 16)
|
||||||
|
}
|
||||||
|
className="w-16"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
}
|
</TabsContent>
|
||||||
|
<TabsContent value="style" className="space-y-4">
|
||||||
else {
|
<div className="flex items-center justify-between">
|
||||||
return (
|
<Label>Text Alignment</Label>
|
||||||
<CardContent className="p-2">
|
<div className="flex space-x-1">
|
||||||
<Tabs defaultValue="text" className="w-full">
|
<Button
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
variant={textAlign === "left" ? "default" : "outline"}
|
||||||
<TabsTrigger value="text">Text</TabsTrigger>
|
size="icon"
|
||||||
<TabsTrigger value="style">Style</TabsTrigger>
|
onClick={() => handleTextAlignChange("left")}
|
||||||
</TabsList>
|
>
|
||||||
|
<AlignLeft className="h-4 w-4" />
|
||||||
<TabsContent value="text" className="space-y-1">
|
</Button>
|
||||||
<div className="space-y-1">
|
<Button
|
||||||
<Label htmlFor="text-content">Text Content</Label>
|
variant={textAlign === "center" ? "default" : "outline"}
|
||||||
<Input
|
size="icon"
|
||||||
id="text-content"
|
onClick={() => handleTextAlignChange("center")}
|
||||||
value={text}
|
>
|
||||||
onChange={(e) => handleTextChange(e.target.value)}
|
<AlignCenter className="h-4 w-4" />
|
||||||
/>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
<div className="space-y-1">
|
variant={textAlign === "right" ? "default" : "outline"}
|
||||||
<Label>Font Family</Label>
|
size="icon"
|
||||||
<Select value={fontFamily} onValueChange={handleFontFamilyChange}>
|
onClick={() => handleTextAlignChange("right")}
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValue placeholder="Select a font" />
|
<AlignRight className="h-4 w-4" />
|
||||||
</SelectTrigger>
|
</Button>
|
||||||
<SelectContent className="h-[250px]">
|
</div>
|
||||||
<SelectGroup>
|
</div>
|
||||||
{fonts.map((font) => (
|
<div className="flex items-center justify-between">
|
||||||
<SelectItem style={{ fontFamily: font }} key={font} value={font}>
|
<Label>Text Style</Label>
|
||||||
{font}
|
<div className="flex space-x-1">
|
||||||
</SelectItem>
|
<Button
|
||||||
))}
|
variant={fontWeight === "bold" ? "default" : "outline"}
|
||||||
</SelectGroup>
|
size="icon"
|
||||||
</SelectContent>
|
onClick={handleFontWeightChange}
|
||||||
</Select>
|
>
|
||||||
</div>
|
<Bold className="h-4 w-4" />
|
||||||
<div className="space-y-1">
|
</Button>
|
||||||
<Label>Font Size</Label>
|
<Button
|
||||||
<div className="flex items-center space-x-2">
|
variant={fontStyle === "italic" ? "default" : "outline"}
|
||||||
<Slider
|
size="icon"
|
||||||
value={[fontSize]}
|
onClick={handleFontStyleChange}
|
||||||
onValueChange={([value]) => handleFontSizeChange(value)}
|
>
|
||||||
min={8}
|
<Italic className="h-4 w-4" />
|
||||||
max={72}
|
</Button>
|
||||||
step={1}
|
<Button
|
||||||
className="flex-grow"
|
variant={underline ? "default" : "outline"}
|
||||||
/>
|
size="icon"
|
||||||
<Input
|
onClick={handleUnderlineChange}
|
||||||
type="number"
|
>
|
||||||
value={fontSize}
|
<Underline className="h-4 w-4" />
|
||||||
onChange={(e) => handleFontSizeChange(parseInt(e.target.value, 10) || 16)}
|
</Button>
|
||||||
className="w-16"
|
<Button
|
||||||
/>
|
variant={linethrough ? "default" : "outline"}
|
||||||
</div>
|
size="icon"
|
||||||
</div>
|
onClick={handleLinethroughChange}
|
||||||
</TabsContent>
|
>
|
||||||
<TabsContent value="style" className="space-y-4">
|
<Strikethrough className="h-4 w-4" />
|
||||||
<div className="flex items-center justify-between">
|
</Button>
|
||||||
<Label>Text Alignment</Label>
|
</div>
|
||||||
<div className="flex space-x-1">
|
</div>
|
||||||
<Button
|
<div className="space-y-2">
|
||||||
variant={textAlign === 'left' ? 'default' : 'outline'}
|
<Label>Line Height</Label>
|
||||||
size="icon"
|
<Slider
|
||||||
onClick={() => handleTextAlignChange('left')}
|
value={[lineHeight]}
|
||||||
>
|
onValueChange={([value]) => handleLineHeightChange(value)}
|
||||||
<AlignLeft className="h-4 w-4" />
|
min={0.5}
|
||||||
</Button>
|
max={3}
|
||||||
<Button
|
step={0.1}
|
||||||
variant={textAlign === 'center' ? 'default' : 'outline'}
|
/>
|
||||||
size="icon"
|
</div>
|
||||||
onClick={() => handleTextAlignChange('center')}
|
<div className="space-y-2">
|
||||||
>
|
<Label>Character Spacing</Label>
|
||||||
<AlignCenter className="h-4 w-4" />
|
<Slider
|
||||||
</Button>
|
value={[charSpacing]}
|
||||||
<Button
|
onValueChange={([value]) => handleCharSpacingChange(value)}
|
||||||
variant={textAlign === 'right' ? 'default' : 'outline'}
|
min={-20}
|
||||||
size="icon"
|
max={100}
|
||||||
onClick={() => handleTextAlignChange('right')}
|
step={1}
|
||||||
>
|
/>
|
||||||
<AlignRight className="h-4 w-4" />
|
</div>
|
||||||
</Button>
|
</TabsContent>
|
||||||
</div>
|
</Tabs>
|
||||||
</div>
|
</CardContent>
|
||||||
<div className="flex items-center justify-between">
|
);
|
||||||
<Label>Text Style</Label>
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
<Button
|
|
||||||
variant={fontWeight === 'bold' ? 'default' : 'outline'}
|
|
||||||
size="icon"
|
|
||||||
onClick={handleFontWeightChange}
|
|
||||||
>
|
|
||||||
<Bold className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={fontStyle === 'italic' ? 'default' : 'outline'}
|
|
||||||
size="icon"
|
|
||||||
onClick={handleFontStyleChange}
|
|
||||||
>
|
|
||||||
<Italic className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={underline ? 'default' : 'outline'}
|
|
||||||
size="icon"
|
|
||||||
onClick={handleUnderlineChange}
|
|
||||||
>
|
|
||||||
<Underline className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={linethrough ? 'default' : 'outline'}
|
|
||||||
size="icon"
|
|
||||||
onClick={handleLinethroughChange}
|
|
||||||
>
|
|
||||||
<Strikethrough className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Line Height</Label>
|
|
||||||
<Slider
|
|
||||||
value={[lineHeight]}
|
|
||||||
onValueChange={([value]) => handleLineHeightChange(value)}
|
|
||||||
min={0.5}
|
|
||||||
max={3}
|
|
||||||
step={0.1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Character Spacing</Label>
|
|
||||||
<Slider
|
|
||||||
value={[charSpacing]}
|
|
||||||
onValueChange={([value]) => handleCharSpacingChange(value)}
|
|
||||||
min={-20}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<Card className="p-2">
|
||||||
<CollapsibleComponent text={"Text Control"}>
|
<CollapsibleComponent text={"Text Control"}>
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
{
|
{previewText && (
|
||||||
previewText &&
|
<div className="p-4 border rounded-md overflow-hidden">
|
||||||
<div className="p-4 border rounded-md overflow-hidden">
|
<p className="font-bold mb-2">Preview:</p>
|
||||||
<p className="font-bold mb-2">Preview:</p>
|
<p
|
||||||
<p style={{
|
style={{
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize: `${fontSize}px`,
|
fontSize: `${fontSize}px`,
|
||||||
fontStyle,
|
fontStyle,
|
||||||
fontWeight,
|
fontWeight,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
letterSpacing: `${charSpacing}px`,
|
letterSpacing: `${charSpacing}px`,
|
||||||
textDecoration: `${underline ? 'underline' : ''} ${linethrough ? 'line-through' : ''}`,
|
textDecoration: `${underline ? "underline" : ""} ${
|
||||||
textAlign
|
linethrough ? "line-through" : ""
|
||||||
}} className='truncate'>
|
}`,
|
||||||
{previewText}
|
textAlign,
|
||||||
</p>
|
}}
|
||||||
</div>
|
className="truncate"
|
||||||
}
|
>
|
||||||
|
{previewText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button onClick={applyChanges} className="w-full">
|
<Button onClick={applyChanges} className="w-full">
|
||||||
Apply Changes
|
Apply Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TextCustomization
|
|
||||||
|
|
||||||
|
export default TextCustomization;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,28 @@
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Text, Square, Upload, Shield, Image, Folder } from "lucide-react";
|
import {
|
||||||
|
Type,
|
||||||
|
Text,
|
||||||
|
Shapes,
|
||||||
|
FolderUp,
|
||||||
|
Shield,
|
||||||
|
Image,
|
||||||
|
FolderKanban,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
|
||||||
const sidebarItems = [
|
const sidebarItems = [
|
||||||
{ id: "design", icon: Text, label: "Design" },
|
{ id: "design", icon: Text, label: "Design" },
|
||||||
{ id: "text", icon: Text, label: "Text" },
|
{ id: "text", icon: Type, label: "Text" },
|
||||||
{ id: "shape", icon: Square, label: "Shape" },
|
{ id: "shape", icon: Shapes, label: "Shape" },
|
||||||
{ id: "upload", icon: Upload, label: "Upload" },
|
{ id: "upload", icon: FolderUp, label: "Upload" },
|
||||||
{ id: "icon", icon: Shield, label: "Icon" },
|
{ id: "icon", icon: Shield, label: "Icon" },
|
||||||
{ id: "image", icon: Image, label: "Image" },
|
{ id: "image", icon: Image, label: "Image" },
|
||||||
{ id: "project", icon: Folder, label: "Project" },
|
{ id: "project", icon: FolderKanban, label: "Project" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ selectedItem, onItemSelect }) {
|
export function Sidebar() {
|
||||||
|
const { selectedPanel, setSelectedPanel } = useContext(CanvasContext);
|
||||||
return (
|
return (
|
||||||
<div className="w-20 border-r h-screen bg-background flex flex-col items-center py-4 gap-6">
|
<div className="w-20 border-r h-screen bg-background flex flex-col items-center py-4 gap-6">
|
||||||
{sidebarItems.map((item) => {
|
{sidebarItems.map((item) => {
|
||||||
|
|
@ -19,10 +30,12 @@ export function Sidebar({ selectedItem, onItemSelect }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => onItemSelect(item.id)}
|
onClick={() => {
|
||||||
|
setSelectedPanel(item.id);
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center gap-1 p-2 rounded-lg w-16 hover:bg-accent",
|
"flex flex-col items-center gap-1 p-2 rounded-lg w-16 hover:bg-accent",
|
||||||
selectedItem === item.id && "bg-accent"
|
selectedPanel === item.id && "bg-accent"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import DesignPanel from "../Panel/DesignPanel";
|
|
||||||
// import TextPanel from "../Panel/TextPanel";
|
|
||||||
import ShapePanel from "../Panel/ShapePanel";
|
|
||||||
|
|
||||||
const panels = {
|
|
||||||
design: DesignPanel,
|
|
||||||
// text: TextPanel,
|
|
||||||
shape: ShapePanel,
|
|
||||||
// Add other panels as needed
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RightPanel({ activePanel }) {
|
|
||||||
const PanelComponent = panels[activePanel] || (() => null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed right-0 top-0 h-screen w-72 border-l bg-background p-4">
|
|
||||||
<div className="flex items-center justify-between border-b pb-4">
|
|
||||||
<h2 className="text-lg font-semibold capitalize">{activePanel}</h2>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<PanelComponent />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,147 @@
|
||||||
|
import { useEffect, useContext } from "react";
|
||||||
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||||
|
import OpenContext from "../Context/openContext/OpenContext";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Card, CardContent } from "../ui/card";
|
||||||
|
|
||||||
export function Canvas() {
|
export function Canvas() {
|
||||||
|
const {
|
||||||
|
setLeftPanelOpen,
|
||||||
|
setRightPanelOpen,
|
||||||
|
setOpenSetting,
|
||||||
|
setOpenObjectPanel,
|
||||||
|
rightPanelOpen,
|
||||||
|
} = useContext(OpenContext);
|
||||||
|
|
||||||
|
const {
|
||||||
|
canvasRef,
|
||||||
|
canvas,
|
||||||
|
setCanvas,
|
||||||
|
fabricCanvasRef,
|
||||||
|
canvasRatio,
|
||||||
|
setCanvasHeight,
|
||||||
|
setCanvasWidth,
|
||||||
|
setScreenWidth,
|
||||||
|
} = useContext(CanvasContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
import("fabric").then((fabricModule) => {
|
||||||
|
window.fabric = fabricModule.fabric;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getRatioValue = (ratio) => {
|
||||||
|
const [width, height] = ratio.split(":").map(Number);
|
||||||
|
return width / height;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateCanvasSize = () => {
|
||||||
|
if (canvasRef.current && canvas) {
|
||||||
|
// Update canvas dimensions
|
||||||
|
const newWidth = canvasRef?.current?.offsetWidth;
|
||||||
|
const newHeight = canvasRef?.current?.offsetHeight;
|
||||||
|
|
||||||
|
canvas.setWidth(newWidth);
|
||||||
|
canvas.setHeight(newHeight);
|
||||||
|
setCanvasWidth(newWidth);
|
||||||
|
setCanvasHeight(newHeight);
|
||||||
|
|
||||||
|
// Adjust the background image to fit the updated canvas size
|
||||||
|
const bgImage = canvas.backgroundImage;
|
||||||
|
if (bgImage) {
|
||||||
|
// Calculate scaling factors for width and height
|
||||||
|
const scaleX = newWidth / bgImage.width;
|
||||||
|
const scaleY = newHeight / bgImage.height;
|
||||||
|
|
||||||
|
// Use the larger scale to cover the entire canvas
|
||||||
|
const scale = Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
|
// Apply scale and position the image
|
||||||
|
bgImage.scaleX = scale;
|
||||||
|
bgImage.scaleY = scale;
|
||||||
|
bgImage.left = 0; // Align left
|
||||||
|
bgImage.top = 0; // Align top
|
||||||
|
|
||||||
|
// Update the background image
|
||||||
|
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
||||||
|
} else {
|
||||||
|
// Render the canvas if no background image
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setScreenWidth(document.getElementById("root").offsetWidth);
|
||||||
|
|
||||||
|
// Handle responsive behavior for panels
|
||||||
|
if (document.getElementById("root").offsetWidth <= 640) {
|
||||||
|
setLeftPanelOpen(false);
|
||||||
|
setRightPanelOpen(false);
|
||||||
|
}
|
||||||
|
if (document.getElementById("root").offsetWidth > 640) {
|
||||||
|
setOpenObjectPanel(false);
|
||||||
|
setOpenSetting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Initial setup
|
||||||
|
updateCanvasSize();
|
||||||
|
|
||||||
|
// Listen for window resize
|
||||||
|
window.addEventListener("resize", updateCanvasSize);
|
||||||
|
|
||||||
|
// Cleanup listener on unmount
|
||||||
|
return () => window.removeEventListener("resize", updateCanvasSize);
|
||||||
|
}, [
|
||||||
|
setCanvasWidth,
|
||||||
|
setCanvasHeight,
|
||||||
|
canvasRatio,
|
||||||
|
canvasRef,
|
||||||
|
canvas,
|
||||||
|
setLeftPanelOpen,
|
||||||
|
setOpenObjectPanel,
|
||||||
|
setRightPanelOpen,
|
||||||
|
rightPanelOpen,
|
||||||
|
setScreenWidth,
|
||||||
|
setOpenSetting,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.fabric) {
|
||||||
|
if (fabricCanvasRef?.current) {
|
||||||
|
fabricCanvasRef?.current.dispose();
|
||||||
|
}
|
||||||
|
// Set styles directly on the canvas element
|
||||||
|
const canvasElement = document.getElementById("fabric-canvas");
|
||||||
|
if (canvasElement) {
|
||||||
|
canvasElement.classList.add("fabric-canvas-container"); // Add the CSS class
|
||||||
|
}
|
||||||
|
|
||||||
|
fabricCanvasRef.current = new window.fabric.Canvas("fabric-canvas", {
|
||||||
|
width: canvasRef?.current?.offsetWidth,
|
||||||
|
height: canvasRef?.current?.offsetWidth,
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
});
|
||||||
|
|
||||||
|
setCanvas(fabricCanvasRef?.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 h-[88vh] bg-[#F5F0FF] p-8 flex flex-col mt-20">
|
<Card className="w-full max-w-3xl p-2 my-4 overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-background rounded-none flex-1 flex flex-col mt-24 mx-auto bg-white pl-5 pb-5 pt-5 border-0 shadow-none">
|
||||||
{/* Main Canvas Area */}
|
<CardContent className="p-0 space-y-2">
|
||||||
<div className="flex-1 bg-white rounded-3xl shadow-sm mb-4 flex items-center justify-center">
|
<AspectRatio
|
||||||
<div className="w-32 h-32 bg-muted rounded-lg" />
|
ratio={getRatioValue(canvasRatio)}
|
||||||
</div>
|
className="overflow-y-scroll shadow-red-200 overflow-x-hidden shadow-lg rounded-lg border-2 border-primary/10 transition-all duration-300 ease-in-out hover:shadow-xl scrollbar-hide"
|
||||||
</div>
|
>
|
||||||
|
<div
|
||||||
|
ref={canvasRef}
|
||||||
|
className="w-full h-full flex items-center justify-center bg-white rounded-md shadow-lg"
|
||||||
|
id="canvas-ref"
|
||||||
|
>
|
||||||
|
<canvas id="fabric-canvas" />
|
||||||
|
</div>
|
||||||
|
</AspectRatio>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/components/Panel/CommonPanel.jsx
Normal file
65
src/components/Panel/CommonPanel.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import SelectObjectFromGroup from "../EachComponent/Customization/SelectObjectFromGroup";
|
||||||
|
import StrokeCustomization from "../EachComponent/Customization/StrokeCustomization";
|
||||||
|
import PositionCustomization from "../EachComponent/Customization/PositionCustomization";
|
||||||
|
import { Card } from "../ui/card";
|
||||||
|
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
|
||||||
|
import OpacityCustomization from "../EachComponent/Customization/OpacityCustomization";
|
||||||
|
import FlipCustomization from "../EachComponent/Customization/FlipCustomization";
|
||||||
|
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
|
||||||
|
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
|
||||||
|
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
|
||||||
|
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
|
||||||
|
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
|
||||||
|
|
||||||
|
const CommonPanel = () => {
|
||||||
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
const activeObject = canvas?.getActiveObject();
|
||||||
|
const customClipPath = activeObject?.isClipPath;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<SelectObjectFromGroup />
|
||||||
|
|
||||||
|
{/* Apply stroke and stroke color */}
|
||||||
|
{!customClipPath && (
|
||||||
|
<>
|
||||||
|
<StrokeCustomization />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeObject?.type !== "group" && (
|
||||||
|
<>
|
||||||
|
<PositionCustomization />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Controls for opacity, flip, and rotation */}
|
||||||
|
<Card className="shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Opacity, Flip, Rotate Control"}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<OpacityCustomization />
|
||||||
|
<FlipCustomization />
|
||||||
|
<RotateCustomization />
|
||||||
|
</div>
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Skew Customization */}
|
||||||
|
<SkewCustomization />
|
||||||
|
|
||||||
|
{/* Scale Objects */}
|
||||||
|
<ScaleObjects />
|
||||||
|
|
||||||
|
{/* Shadow Customization */}
|
||||||
|
<ShadowCustomization />
|
||||||
|
|
||||||
|
{/* Add image into shape */}
|
||||||
|
<AddImageIntoShape />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommonPanel;
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { Label } from "../ui/label";
|
|
||||||
import { Slider } from "../ui/slider";
|
|
||||||
|
|
||||||
export default function DesignPanel() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 pt-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Shadow Style</Label>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="aspect-square rounded-lg bg-muted hover:bg-muted/80"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Offset X</Label>
|
|
||||||
<Slider defaultValue={[0]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Offset Y</Label>
|
|
||||||
<Slider defaultValue={[0]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Blur</Label>
|
|
||||||
<Slider defaultValue={[0]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Opacity</Label>
|
|
||||||
<Slider defaultValue={[100]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +1,28 @@
|
||||||
import { Plus, Search, Upload } from "lucide-react";
|
import { useContext } from "react";
|
||||||
import { Button } from "../ui/button";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { Input } from "../ui/input";
|
import TextPanel from "./TextPanel";
|
||||||
|
|
||||||
export function EditorPanel({ type }) {
|
const EditorPanel = () => {
|
||||||
if (!type) return null;
|
const { selectedPanel } = useContext(CanvasContext);
|
||||||
|
|
||||||
const panelContent = {
|
const renderPanel = () => {
|
||||||
text: (
|
switch (selectedPanel) {
|
||||||
<>
|
case "text":
|
||||||
<h2 className="text-lg font-semibold mb-4">Text</h2>
|
return <TextPanel />;
|
||||||
<Button className="w-full">
|
default:
|
||||||
<Plus className="mr-2 h-4 w-4" /> Add Text
|
return;
|
||||||
</Button>
|
}
|
||||||
<div className="mt-4 space-y-2">
|
|
||||||
<Input placeholder="Add a Heading" />
|
|
||||||
<Input placeholder="Add a Subheading" />
|
|
||||||
<Input placeholder="Add body text" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
shape: (
|
|
||||||
<>
|
|
||||||
<h2 className="text-lg font-semibold mb-4">Shape</h2>
|
|
||||||
<div className="relative mb-4">
|
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input placeholder="Search shapes" className="pl-8" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<div key={i} className="aspect-square bg-muted rounded-md" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
image: (
|
|
||||||
<>
|
|
||||||
<h2 className="text-lg font-semibold mb-4">Image</h2>
|
|
||||||
<Button className="w-full">
|
|
||||||
<Upload className="mr-2 h-4 w-4" /> Upload Image
|
|
||||||
</Button>
|
|
||||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
|
||||||
{[...Array(4)].map((_, i) => (
|
|
||||||
<div key={i} className="aspect-square bg-muted rounded-md" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 bg-background border-r border-border p-4 overflow-y-auto">
|
<>
|
||||||
{panelContent[type]}
|
{selectedPanel !== "" && (
|
||||||
</div>
|
<div className="w-80 h-[calc(100vh-32px)] bg-background rounded-xl shadow-lg mx-4 my-4">
|
||||||
|
{renderPanel()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default EditorPanel;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { Input } from "../ui/input";
|
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
|
||||||
|
|
||||||
export default function ShapePanel() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 pt-4">
|
|
||||||
<Input placeholder="Search with project name" />
|
|
||||||
<ScrollArea className="h-[calc(100vh-8rem)]">
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{Array.from({ length: 9 }).map((_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="aspect-square rounded-md bg-muted hover:bg-muted/80"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +1,92 @@
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
import { Input } from "../ui/Input";
|
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
|
import { fabric } from "fabric";
|
||||||
|
import CommonPanel from "./CommonPanel";
|
||||||
|
import TextCustomization from "../EachComponent/Customization/TextCustomization";
|
||||||
|
|
||||||
export default function TextPanel() {
|
export default function TextPanel() {
|
||||||
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
const activeObject = canvas?.getActiveObject();
|
||||||
|
const activeObjectType = activeObject?.type;
|
||||||
|
const hasClipPath = !!activeObject?.clipPath;
|
||||||
|
const customClipPath = activeObject?.isClipPath;
|
||||||
|
|
||||||
|
const addText = () => {
|
||||||
|
if (canvas) {
|
||||||
|
const text = new fabric.IText("Editable Text", {
|
||||||
|
left: 100,
|
||||||
|
top: 100,
|
||||||
|
fontFamily: "Poppins",
|
||||||
|
fontSize: 16,
|
||||||
|
});
|
||||||
|
// Add the text to the canvas and re-render
|
||||||
|
canvas.add(text);
|
||||||
|
// canvas.clipPath = text;
|
||||||
|
canvas.setActiveObject(text);
|
||||||
|
setActiveObject(text);
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 h-[calc(100vh-32px)] bg-background rounded-3xl shadow-lg mx-4 my-4">
|
<div>
|
||||||
<ScrollArea className="h-[calc(100vh-32px)] px-4 py-4">
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<h2 className="text-lg font-semibold">Text</h2>
|
||||||
<h2 className="text-lg font-semibold">Text</h2>
|
<Button variant="ghost" size="icon">
|
||||||
<Button variant="ghost" size="icon">
|
<X className="h-4 w-4" />
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button className="w-full bg-[#FF4D8D] hover:bg-[#FF3D7D] text-white rounded-2xl mb-6 h-12 text-base font-medium">
|
|
||||||
Add Text
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<ScrollArea className="h-[calc(100vh-115px)] px-4 py-4">
|
||||||
<div>
|
<Button
|
||||||
<p className="text-sm text-muted-foreground mb-2">
|
className="w-full bg-[#FF2B85] hover:bg-[#FF2B85] text-white rounded-[10px] mb-6 h-12 font-medium text-xl"
|
||||||
Default text style
|
onClick={() => {
|
||||||
</p>
|
addText();
|
||||||
<Input
|
}}
|
||||||
placeholder="Add a Heading H1"
|
>
|
||||||
className="mb-2 rounded-2xl h-12"
|
Add Text
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.5 18.3333H12.5C16.6667 18.3333 18.3333 16.6666 18.3333 12.5V7.49996C18.3333 3.33329 16.6667 1.66663 12.5 1.66663H7.5C3.33333 1.66663 1.66667 3.33329 1.66667 7.49996V12.5C1.66667 16.6666 3.33333 18.3333 7.5 18.3333Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
<Input
|
<path
|
||||||
placeholder="Add a Subheading H2"
|
d="M5.83333 7.40837C8.45833 6.10004 11.5417 6.10004 14.1667 7.40837"
|
||||||
className="mb-2 rounded-2xl h-12"
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
<Input
|
<path
|
||||||
placeholder="Add Some body text"
|
d="M10 13.5833V6.60828"
|
||||||
className="rounded-2xl h-12"
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
{activeObject ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<CommonPanel />
|
||||||
|
<TextCustomization />
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div>
|
<p className="text-sm font-semibold text-center">
|
||||||
<div className="flex justify-between items-center mb-2">
|
No active object found
|
||||||
<p className="text-sm text-muted-foreground">Text Design</p>
|
</p>
|
||||||
<Button variant="link" size="sm">
|
)}
|
||||||
See all
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<div key={i} className="aspect-square bg-muted rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue