added text section design layout

This commit is contained in:
smfahim25 2025-01-26 17:26:25 +06:00
parent 976978aec4
commit 954ac950b0
21 changed files with 2156 additions and 1822 deletions

View file

@ -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 />

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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" />

View file

@ -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>
);
}

View file

@ -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>
); );
} }

View 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;

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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>
); );