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 Canvas from "./components/Canvas";
import WebFont from "webfontloader";
@ -8,12 +8,12 @@ import SheetLeftPanel from "./components/Layouts/SheetLeftPanel";
import CanvasCapture from "./components/CanvasCapture";
import { Toaster } from "./components/ui/toaster";
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 TextPanel from "./components/Panel/TextPanel";
import { TopBar } from "./components/Panel/TopBar";
import { ActionButtons } from "./components/ActionButtons";
import EditorPanel from "./components/Panel/EditorPanel";
import CanvasContext from "./components/Context/canvasContext/CanvasContext";
function App() {
useEffect(() => {
@ -74,7 +74,7 @@ function App() {
});
}, []);
const [selectedItem, setSelectedItem] = useState("text");
const { selectedPanel } = useContext(CanvasContext);
const [hasSelectedObject, setHasSelectedObject] = useState(true);
return (
@ -93,8 +93,8 @@ function App() {
// </div>
<div className="flex h-screen">
<Sidebar selectedItem={selectedItem} onItemSelect={setSelectedItem} />
{selectedItem === "text" && <TextPanel />}
<Sidebar />
{selectedPanel !== "" && <EditorPanel />}
<div className="flex-1 relative">
<TopBar isVisible={hasSelectedObject} />
<ActionButtons />

View file

@ -1,44 +1,58 @@
import { ChevronDown } from "lucide-react";
import CanvasContext from "./Context/canvasContext/CanvasContext";
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() {
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
const handleRatioChange = (newRatio) => {
setCanvasRatio(newRatio);
};
return (
<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">
<Button variant="ghost" className="gap-2 h-9 px-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<g clipPath="url(#clip0_71_2678)">
<path d="M1 3.5L11 3.5" stroke="black" strokeLinecap="round" />
<path
d="M1 8.5769L11 8.57691"
stroke="black"
strokeLinecap="round"
/>
<path
d="M9.0769 1L9.0769 11"
stroke="black"
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 className="w-full">
<Select onValueChange={handleRatioChange} value={canvasRatio}>
<SelectTrigger className="w-full text-xs font-bold">
<SelectValue placeholder="Select aspect ratio" />
</SelectTrigger>
<SelectContent>
{aspectRatios.map((ratio) => (
<SelectItem
key={ratio.value}
value={ratio.value}
className="text-xs font-bold"
>
{ratio.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="mr-5">
<Button

View file

@ -1,19 +1,38 @@
import { useRef, useState } from 'react'
import CanvasContext from './CanvasContext';
import { useRef, useState } from "react";
import CanvasContext from "./CanvasContext";
const CanvasContextProvider = ({ children }) => {
const canvasRef = useRef(null);
const [canvas, setCanvas] = useState(null);
const [canvasHeight, setCanvasHeight] = useState(0);
const [canvasWidth, setCanvasWidth] = useState(0);
const [screenWidth, setScreenWidth] = useState(0);
const fabricCanvasRef = useRef(null);
const canvasRef = useRef(null);
const [canvas, setCanvas] = useState(null);
const [canvasHeight, setCanvasHeight] = useState(0);
const [canvasWidth, setCanvasWidth] = useState(0);
const [screenWidth, setScreenWidth] = useState(0);
const [canvasRatio, setCanvasRatio] = useState("4:3");
const [selectedPanel, setSelectedPanel] = useState("");
const fabricCanvasRef = useRef(null);
return (
<CanvasContext.Provider value={{ canvasRef, canvas, setCanvas, fabricCanvasRef, canvasHeight, setCanvasHeight, canvasWidth, setCanvasWidth, screenWidth, setScreenWidth }}>
{children}
</CanvasContext.Provider>
)
}
return (
<CanvasContext.Provider
value={{
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 CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { useContext, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { fabric } from 'fabric';
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { useContext, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { fabric } from "fabric";
import Resizer from "react-image-file-resizer";
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } 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';
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} 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 = [
{ 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: RotateCw, title: 'Quality & Rotation', description: 'Adjust image quality and rotation as needed' },
{ icon: FileType, title: 'Format Conversion', description: 'Convert between various image formats' },
]
{
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: 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 { canvas } = useContext(CanvasContext);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const [errorMessage, setErrorMessage] = useState("");
const [width, setWidth] = useState(1080);
const [height, setHeight] = useState(1080);
const [quality, setQuality] = useState(100);
const [rotation, setRotation] = useState("0");
const [format, setFormat] = useState('JPEG');
const fileInputRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const { canvas } = useContext(CanvasContext);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const [errorMessage, setErrorMessage] = useState("");
const [width, setWidth] = useState(1080);
const [height, setHeight] = useState(1080);
const [quality, setQuality] = useState(100);
const [rotation, setRotation] = useState("0");
const [format, setFormat] = useState("JPEG");
const fileInputRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const handleResize = (file, callback) => {
Resizer.imageFileResizer(
file,
width,
height,
format,
quality,
parseInt(rotation),
(resizedFile) => {
callback(resizedFile); // Pass the resized file to the callback
},
'file' // Output type
);
};
const handleResize = (file, callback) => {
Resizer.imageFileResizer(
file,
width,
height,
format,
quality,
parseInt(rotation),
(resizedFile) => {
callback(resizedFile); // Pass the resized file to the callback
},
"file" // Output type
);
};
const fileHandler = (e) => {
const file = e.target.files[0];
if (!file) {
setErrorMessage("No file selected.");
return;
}
const fileHandler = (e) => {
const file = e.target.files[0];
if (!file) {
setErrorMessage("No file selected.");
return;
}
// Check if the file is an SVG
if (file.type === "image/svg+xml") {
// Add SVG directly to canvas without compression
const blobUrl = URL.createObjectURL(file);
const imgElement = new Image();
imgElement.src = blobUrl;
// Check if the file is an SVG
if (file.type === "image/svg+xml") {
// Add SVG directly to canvas without compression
const blobUrl = URL.createObjectURL(file);
const imgElement = new Image();
imgElement.src = blobUrl;
imgElement.onload = () => {
handleImageInsert(imgElement); // Insert the image without resizing
URL.revokeObjectURL(blobUrl);
clearFileInput();
};
imgElement.onload = () => {
handleImageInsert(imgElement); // Insert the image without resizing
URL.revokeObjectURL(blobUrl);
clearFileInput();
};
imgElement.onerror = () => {
console.error("Failed to load SVG.");
setErrorMessage("Failed to load SVG.");
URL.revokeObjectURL(blobUrl);
clearFileInput();
};
return;
}
imgElement.onerror = () => {
console.error("Failed to load SVG.");
setErrorMessage("Failed to load SVG.");
URL.revokeObjectURL(blobUrl);
clearFileInput();
};
return;
}
// Handle raster images (JPEG, PNG, etc.)
const imgElement = new Image();
const blobUrl = URL.createObjectURL(file);
imgElement.src = blobUrl;
// Handle raster images (JPEG, PNG, etc.)
const imgElement = new Image();
const blobUrl = URL.createObjectURL(file);
imgElement.src = blobUrl;
imgElement.onload = () => {
// If the width is greater than 1080px, compress the image
if (imgElement.width > 1080) {
handleResize(file, (compressedFile) => {
const compressedBlobUrl = URL.createObjectURL(compressedFile);
const compressedImg = new Image();
compressedImg.src = compressedBlobUrl;
imgElement.onload = () => {
// If the width is greater than 1080px, compress the image
if (imgElement.width > 1080) {
handleResize(file, (compressedFile) => {
const compressedBlobUrl = URL.createObjectURL(compressedFile);
const compressedImg = new Image();
compressedImg.src = compressedBlobUrl;
compressedImg.onload = () => {
handleImageInsert(compressedImg); // Insert the resized image
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
compressedImg.onload = () => {
handleImageInsert(compressedImg); // Insert the resized image
URL.revokeObjectURL(compressedBlobUrl); // Clean up
clearFileInput();
};
};
imgElement.onerror = () => {
console.error("Failed to load image.");
setErrorMessage("Failed to load image.");
URL.revokeObjectURL(blobUrl);
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();
};
const clearFileInput = () => {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
imgElement.onerror = () => {
console.error("Failed to load image.");
setErrorMessage("Failed to load image.");
URL.revokeObjectURL(blobUrl);
clearFileInput();
};
};
const handleImageInsert = (img) => {
if (!activeObject) {
setErrorMessage("No active object selected!");
return;
}
// Ensure absolute positioning for the clipPath
activeObject.set({
isClipPath: true, // Custom property
absolutePositioned: true,
});
const clearFileInput = () => {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
// Calculate scale factors based on clip object size
let scaleX = activeObject.width / img.width;
let scaleY = activeObject.height / img.height;
if (activeObject?.width < 100) {
scaleX = 0.2;
}
const handleImageInsert = (img) => {
if (!activeObject) {
setErrorMessage("No active object selected!");
return;
}
// Ensure absolute positioning for the clipPath
activeObject.set({
isClipPath: true, // Custom property
absolutePositioned: true,
});
if (activeObject.height < 100) {
scaleY = 0.2;
}
// Calculate scale factors based on clip object size
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
const fabricImage = new fabric.Image(img, {
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
});
if (activeObject.height < 100) {
scaleY = 0.2;
}
// Adjust position based on the clipPath's transformations
fabricImage.set({
left: activeObject.left,
top: activeObject.top,
angle: activeObject.angle // Match rotation if any
});
// Create a fabric image object with scaling and clipPath
const fabricImage = new fabric.Image(img, {
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
});
canvas.add(fabricImage);
canvas.setActiveObject(fabricImage);
setActiveObject(fabricImage);
canvas.renderAll();
};
// Adjust position based on the clipPath's transformations
fabricImage.set({
left: activeObject.left,
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 = () => {
return (
<div>
// canvas.remove(activeObject);
{/* <Card className="my-2">
const content = () => {
return (
<div>
{/* <Card className="my-2">
<CardContent className="p-0">
<Alert>
<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>
</Card> */}
<Card className="my-2">
<CardContent className="p-0">
<Alert>
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">
Image Insertion <ImagePlus className="h-5 w-5" />
</AlertTitle>
<AlertDescription className="mt-1">
<p className="mb-1">
Insert and customize images within shapes. Adjust image position and clipping after insertion.
</p>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between mt-2">
<span>Key Features</span>
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="grid grid-cols-1 gap-2 mt-2">
{features.map((feature, index) => (
<div
key={index}
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} />
<Card className="my-2">
<CardContent className="p-0">
<Alert>
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">
Image Insertion <ImagePlus className="h-5 w-5" />
</AlertTitle>
<AlertDescription className="mt-1">
<p className="mb-1">
Insert and customize images within shapes. Adjust image
position and clipping after insertion.
</p>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<Button
variant="outline"
className="w-full justify-between mt-2"
>
<span>Key Features</span>
{isOpen ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</div>
</div>
)
}
return (
<Card className="flex flex-col p-2">
<CollapsibleComponent text={"Insert Image"}>
{content()}
</CollapsibleComponent>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="grid grid-cols-1 gap-2 mt-2">
{features.map((feature, index) => (
<div
key={index}
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>
</div>
</div>
);
};
return (
<Card className="flex flex-col shadow-none border-0">
<CollapsibleComponent text={"Insert Image"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default AddImageIntoShape;

View file

@ -1,6 +1,8 @@
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ChevronUp, ChevronDown } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { useEffect, useState } from "react";
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
if (text === "Canvas Setting") {
setIsOpen(false);
} else {
setIsOpen(true);
}
else {
setIsOpen(true)
}
}, [text])
}, [text]);
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<div className={`flex items-center justify-between cursor-pointer ${!isOpen ? "my-2" : ""}`}>
<h2 className='font-bold'>{text}</h2>
<Button variant={"outline"}>
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
<div
className={`flex items-center justify-between cursor-pointer ${
!isOpen ? "my-2" : ""
}`}
>
<h2 className="font-bold mb-2">{text}</h2>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
{children}
</CollapsibleContent>
<CollapsibleContent>{children}</CollapsibleContent>
</Collapsible>
)
}
);
};
export default CollapsibleComponent
export default CollapsibleComponent;

View file

@ -1,68 +1,73 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast';
import { useContext, useEffect, useState } from 'react';
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { useContext, useEffect, useState } from "react";
import { Lock, Unlock } from "lucide-react";
import { Card } from '@/components/ui/card';
import { Card } from "@/components/ui/card";
const LockObject = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [isLocked, setIsLocked] = useState(false);
const { toast } = useToast();
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [isLocked, setIsLocked] = useState(false);
const { toast } = useToast();
useEffect(() => {
if (activeObject?.lockMovementX === false) {
setIsLocked(false);
}
else {
setIsLocked(true);
}
}, [activeObject])
useEffect(() => {
if (activeObject?.lockMovementX === false) {
setIsLocked(false);
} else {
setIsLocked(true);
}
}, [activeObject]);
const toggleLock = () => {
if (!canvas || !activeObject) {
toast({
title: "No object selected",
description: "Please select an object to lock or unlock.",
variant: "destructive",
})
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.",
})
const toggleLock = () => {
if (!canvas || !activeObject) {
toast({
title: "No object selected",
description: "Please select an object to lock or unlock.",
variant: "destructive",
});
return;
}
return (
<Card className="p-2">
<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>
)
}
const newLockState = !activeObject.lockMovementX;
activeObject.set({
lockMovementX: newLockState,
lockMovementY: newLockState,
lockRotation: newLockState,
lockScalingX: newLockState,
lockScalingY: newLockState,
});
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 CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { useContext, useEffect, useState } from 'react';
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, ChevronUp, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import CollapsibleComponent from './CollapsibleComponent';
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { useContext, useEffect, useState } from "react";
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import CollapsibleComponent from "./CollapsibleComponent";
const PositionCustomization = () => {
const { canvas } = useContext(CanvasContext)
const { activeObject } = useContext(ActiveObjectContext)
const { canvas } = useContext(CanvasContext);
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) {
setObjectPosition({
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()
}
useEffect(() => {
if (activeObject) {
setObjectPosition({
left: Math.round(activeObject.left || 0),
top: Math.round(activeObject.top || 0),
});
}
}, [activeObject]);
const handleInputChange = (key, value) => {
const numValue = parseInt(value, 10)
if (!isNaN(numValue)) {
updateObjectPosition(key, numValue)
}
const updateObjectPosition = (key, value) => {
const updatedPosition = { ...objectPosition, [key]: value };
setObjectPosition(updatedPosition);
if (canvas && activeObject) {
activeObject.set(updatedPosition);
canvas.renderAll();
}
};
const handleSliderChange = (key, value) => {
updateObjectPosition(key, value[0])
const handleInputChange = (key, value) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue)) {
updateObjectPosition(key, numValue);
}
};
const adjustPosition = (key, delta) => {
const newValue = objectPosition[key] + delta
updateObjectPosition(key, newValue)
}
const handleSliderChange = (key, value) => {
updateObjectPosition(key, value[0]);
};
const content = () => {
return (
<CardContent className="space-y-6 p-0">
<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 adjustPosition = (key, delta) => {
const newValue = objectPosition[key] + delta;
updateObjectPosition(key, newValue);
};
const content = () => {
return (
<Card className="p-2">
<CollapsibleComponent text={"Position Control"}>
{content()}
</CollapsibleComponent>
</Card>
)
}
<CardContent className="space-y-6 p-0">
<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>
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";
const ScaleObjects = () => {
const { canvas } = useContext(CanvasContext); // Access Fabric.js canvas from context
const { activeObject } = useContext(ActiveObjectContext);
const [scaleX, setScaleX] = useState(1);
const [scaleY, setScaleY] = useState(1);
const { canvas } = useContext(CanvasContext); // Access Fabric.js canvas from context
const { activeObject } = useContext(ActiveObjectContext);
const [scaleX, setScaleX] = useState(1);
const [scaleY, setScaleY] = useState(1);
useEffect(() => {
if (activeObject) {
setScaleX(activeObject?.scaleX);
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>
)
useEffect(() => {
if (activeObject) {
setScaleX(activeObject?.scaleX);
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 (
<Card className="grid items-center gap-2 p-2">
<CollapsibleComponent text={"Scale Control"}>
{content()}
</CollapsibleComponent>
</Card>
<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>
);
};
return (
<Card className="grid items-center gap-2 shadow-none border-0">
<CollapsibleComponent text={"Scale Control"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default ScaleObjects;

View file

@ -1,151 +1,159 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import { Select, SelectContent, SelectItem, SelectTrigger, 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';
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
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 [groupObjects, setGroupObjects] = useState([])
const [selectedObject, setSelectedObject] = useState(null)
const { setActiveObject } = useContext(ActiveObjectContext)
const { canvas } = useContext(CanvasContext)
const previewCanvasRef = useRef(null)
const [groupObjects, setGroupObjects] = useState([]);
const [selectedObject, setSelectedObject] = useState(null);
const { setActiveObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
const previewCanvasRef = useRef(null);
const activeObject = canvas?.getActiveObject()
const activeObject = canvas?.getActiveObject();
useEffect(() => {
if (activeObject?.type === 'group') {
setGroupObjects(activeObject._objects || [])
setSelectedObject(null)
} else {
setGroupObjects([])
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)
useEffect(() => {
if (activeObject?.type === "group") {
setGroupObjects(activeObject._objects || []);
setSelectedObject(null);
} else {
setGroupObjects([]);
setSelectedObject(null);
}
}, [activeObject]);
if (!activeObject || activeObject.type !== 'group') {
return null
// 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]);
return (
<div>
<Card className="p-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>
const handleSelectObject = (value) => {
const selected = groupObjects[parseInt(value)];
setSelectedObject(selected);
setActiveObject(selected);
};
{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>
</Card>
<Separator className="my-4" />
if (!activeObject || activeObject.type !== "group") {
return null;
}
return (
<div>
<Card className="p-4 shadow-none border-0">
<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>
)
}
export default SelectObjectFromGroup;
</Card>
<Separator className="my-4" />
</div>
);
};
export default SelectObjectFromGroup;

View file

@ -9,135 +9,143 @@ import { useContext, useEffect, useState } from "react";
import CollapsibleComponent from "./CollapsibleComponent";
const ShadowCustomization = () => {
// get values from context
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
// get values from context
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
// state to handle shadow inputs
const [shadowColor, setShadowColor] = useState("");
const [offsetX, setOffsetX] = useState(5);
const [offsetY, setOffsetY] = useState(5);
const [blur, setBlur] = useState(0.5);
const [opacity, setOpacity] = useState(0.5);
// state to handle shadow inputs
const [shadowColor, setShadowColor] = useState("");
const [offsetX, setOffsetX] = useState(5);
const [offsetY, setOffsetY] = useState(5);
const [blur, setBlur] = useState(0.5);
const [opacity, setOpacity] = useState(0.5);
useEffect(() => {
if (activeObject) {
setShadowColor(activeObject?.shadow?.color || "#db1d62");
setOffsetX(activeObject?.shadow?.offsetX || 5);
setOffsetY(activeObject?.shadow?.offsetY || 5);
setBlur(activeObject?.shadow?.blur || 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
}
useEffect(() => {
if (activeObject) {
setShadowColor(activeObject?.shadow?.color || "#db1d62");
setOffsetX(activeObject?.shadow?.offsetX || 5);
setOffsetY(activeObject?.shadow?.offsetY || 5);
setBlur(activeObject?.shadow?.blur || 0.5);
setOpacity(activeObject?.shadow?.opacity || 0.5);
}
}, [activeObject]);
const handleDisableShadow = () => {
if (activeObject && canvas) {
activeObject.set("shadow", null)
canvas.renderAll()
}
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
}
};
const content = () => {
return (
<div>
<div className="space-y-2">
<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 handleDisableShadow = () => {
if (activeObject && canvas) {
activeObject.set("shadow", null);
canvas.renderAll();
}
};
const content = () => {
return (
<Card className='p-2'>
<CollapsibleComponent text={"Shadow Control"}>
{content()}
</CollapsibleComponent>
</Card>
<div>
<div className="space-y-2">
<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>
);
};
return (
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Shadow Control"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default ShadowCustomization;

View file

@ -1,85 +1,85 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Card } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { useContext, useEffect, useState } from 'react'
import CollapsibleComponent from './CollapsibleComponent';
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { useContext, useEffect, useState } from "react";
import CollapsibleComponent from "./CollapsibleComponent";
const SkewCustomization = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [skewX, setSkewX] = useState(0);
const [skewY, setSkewY] = useState(0);
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [skewX, setSkewX] = useState(0);
const [skewY, setSkewY] = useState(0);
useEffect(() => {
if (activeObject) {
setSkewX(activeObject?.skewX);
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>
)
useEffect(() => {
if (activeObject) {
setSkewX(activeObject?.skewX);
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 (
<Card className="p-2">
<CollapsibleComponent text={"Skew Control"}>
{content()}
</CollapsibleComponent>
</Card>
)
}
<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>
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 { fabric } from 'fabric'
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext'
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'
import { Card, CardContent } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Slider } from '@/components/ui/slider'
import { Button } from '@/components/ui/button'
import { Paintbrush, ContrastIcon as Gradient } from 'lucide-react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import CollapsibleComponent from './CollapsibleComponent'
import { useContext, useState, useRef, useEffect } from "react";
import { fabric } from "fabric";
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
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 { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
const previewRef = useRef(null);
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
const previewRef = useRef(null);
const [strokeWidth, setStrokeWidth] = useState(0);
const [strokeColor, setStrokeColor] = useState("#000000");
const [gradientStrokeColors, setGradientStrokeColors] = useState({
color1: "#f97316",
color2: "#e26286",
});
const [colorType, setColorType] = useState("color");
const [gradientDirection, setGradientDirection] = useState("to bottom");
const [strokeWidth, setStrokeWidth] = useState(0);
const [strokeColor, setStrokeColor] = useState("#000000");
const [gradientStrokeColors, setGradientStrokeColors] = useState({
color1: "#f97316",
color2: "#e26286",
});
const [colorType, setColorType] = useState("color");
const [gradientDirection, setGradientDirection] = useState("to bottom");
// Utility function to handle styles of objects
const handleObjectStyle = (object) => {
// Determine fill type (solid or gradient)
if (object.stroke) {
if (typeof object.stroke === "string") {
setColorType("color");
setStrokeColor(object.stroke); // Solid color fill
} else if (object.stroke.colorStops) {
setColorType("gradient");
setGradientStrokeColors({
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
}
// Utility function to handle styles of objects
const handleObjectStyle = (object) => {
if (object.stroke) {
if (typeof object.stroke === "string") {
setColorType("color");
setStrokeColor(object.stroke); // Solid color fill
} else if (object.stroke.colorStops) {
setColorType("gradient");
setGradientStrokeColors({
color1: object.stroke.colorStops[0]?.color || "#f97316",
color2: object.stroke.colorStops[1]?.color || "#e26286",
});
}
}
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
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',
};
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>
)
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();
};
// 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 (
<Card className="p-2">
<CollapsibleComponent text={"Stroke Control"}>
{content()}
</CollapsibleComponent>
</Card>
<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" 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;

View file

@ -1,314 +1,382 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectGroup, SelectItem, 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';
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
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 = [
'Roboto', 'Open Sans', 'Lato', 'Montserrat', 'Raleway', 'Poppins', 'Merriweather',
'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',
"Roboto",
"Open Sans",
"Lato",
"Montserrat",
"Raleway",
"Poppins",
"Merriweather",
"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 = () => {
// get values from context
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
// get values from context
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
const [text, setText] = useState('');
const [fontFamily, setFontFamily] = useState('Arial');
const [fontSize, setFontSize] = useState(20);
const [fontStyle, setFontStyle] = useState('normal');
const [fontWeight, setFontWeight] = useState('normal');
const [lineHeight, setLineHeight] = useState(1.16);
const [charSpacing, setCharSpacing] = useState(0);
const [underline, setUnderline] = useState(false);
const [linethrough, setLinethrough] = useState(false);
const [textAlign, setTextAlign] = useState('left');
const [previewText, setPreviewText] = useState('');
const [text, setText] = useState("");
const [fontFamily, setFontFamily] = useState("Arial");
const [fontSize, setFontSize] = useState(20);
const [fontStyle, setFontStyle] = useState("normal");
const [fontWeight, setFontWeight] = useState("normal");
const [lineHeight, setLineHeight] = useState(1.16);
const [charSpacing, setCharSpacing] = useState(0);
const [underline, setUnderline] = useState(false);
const [linethrough, setLinethrough] = useState(false);
const [textAlign, setTextAlign] = useState("left");
const [previewText, setPreviewText] = useState("");
useEffect(() => {
if (activeObject?.type === "i-text") {
setText(activeObject?.text || '');
setFontFamily(activeObject?.fontFamily || 'Arial');
setFontSize(activeObject?.fontSize || 20);
setFontStyle(activeObject?.fontStyle || 'normal');
setFontWeight(activeObject?.fontWeight || 'normal');
setLineHeight(activeObject?.lineHeight || 1.16);
setCharSpacing(activeObject?.charSpacing || 0);
setUnderline(activeObject?.underline || false);
setLinethrough(activeObject?.linethrough || false);
setTextAlign(activeObject?.textAlign || 'left');
setPreviewText(activeObject?.text || '');
}
}, [activeObject])
const updateActiveObject = (properties) => {
if (activeObject?.type === "i-text") {
activeObject.set(properties)
canvas?.renderAll()
}
useEffect(() => {
if (activeObject?.type === "i-text") {
setText(activeObject?.text || "");
setFontFamily(activeObject?.fontFamily || "Arial");
setFontSize(activeObject?.fontSize || 20);
setFontStyle(activeObject?.fontStyle || "normal");
setFontWeight(activeObject?.fontWeight || "normal");
setLineHeight(activeObject?.lineHeight || 1.16);
setCharSpacing(activeObject?.charSpacing || 0);
setUnderline(activeObject?.underline || false);
setLinethrough(activeObject?.linethrough || false);
setTextAlign(activeObject?.textAlign || "left");
setPreviewText(activeObject?.text || "");
}
}, [activeObject]);
const applyChanges = () => {
updateActiveObject({
text,
fontFamily,
fontSize,
fontStyle,
fontWeight,
lineHeight,
charSpacing,
underline,
linethrough,
textAlign
});
const updateActiveObject = (properties) => {
if (activeObject?.type === "i-text") {
activeObject.set(properties);
canvas?.renderAll();
}
};
const handleTextChange = (newText) => {
setText(newText);
setPreviewText(newText);
}
const applyChanges = () => {
updateActiveObject({
text,
fontFamily,
fontSize,
fontStyle,
fontWeight,
lineHeight,
charSpacing,
underline,
linethrough,
textAlign,
});
};
const handleFontFamilyChange = (newFontFamily) => {
setFontFamily(newFontFamily);
}
const handleTextChange = (newText) => {
setText(newText);
setPreviewText(newText);
};
const handleFontSizeChange = (newFontSize) => {
setFontSize(newFontSize)
}
const handleFontFamilyChange = (newFontFamily) => {
setFontFamily(newFontFamily);
};
const handleTextAlignChange = (newTextAlign) => {
setTextAlign(newTextAlign)
}
const handleFontSizeChange = (newFontSize) => {
setFontSize(newFontSize);
};
const handleFontStyleChange = () => {
const newFontStyle = fontStyle === 'normal' ? 'italic' : 'normal'
setFontStyle(newFontStyle)
}
const handleTextAlignChange = (newTextAlign) => {
setTextAlign(newTextAlign);
};
const handleFontWeightChange = () => {
const newFontWeight = fontWeight === 'normal' ? 'bold' : 'normal'
setFontWeight(newFontWeight)
}
const handleFontStyleChange = () => {
const newFontStyle = fontStyle === "normal" ? "italic" : "normal";
setFontStyle(newFontStyle);
};
const handleLineHeightChange = (newLineHeight) => {
setLineHeight(newLineHeight)
}
const handleFontWeightChange = () => {
const newFontWeight = fontWeight === "normal" ? "bold" : "normal";
setFontWeight(newFontWeight);
};
const handleCharSpacingChange = (newCharSpacing) => {
setCharSpacing(newCharSpacing)
}
const handleLineHeightChange = (newLineHeight) => {
setLineHeight(newLineHeight);
};
const handleUnderlineChange = () => {
const newUnderline = !underline
setUnderline(newUnderline)
}
const handleCharSpacingChange = (newCharSpacing) => {
setCharSpacing(newCharSpacing);
};
const handleLinethroughChange = () => {
const newLinethrough = !linethrough
setLinethrough(newLinethrough)
}
const handleUnderlineChange = () => {
const newUnderline = !underline;
setUnderline(newUnderline);
};
const content = () => {
if (!(activeObject?.type === "i-text")) {
return (
<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>
const handleLinethroughChange = () => {
const newLinethrough = !linethrough;
setLinethrough(newLinethrough);
};
const content = () => {
if (!(activeObject?.type === "i-text")) {
return (
<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>
)
}
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>
</TabsContent>
<TabsContent value="style" className="space-y-4">
<div className="flex items-center justify-between">
<Label>Text Alignment</Label>
<div className="flex space-x-1">
<Button
variant={textAlign === 'left' ? 'default' : 'outline'}
size="icon"
onClick={() => handleTextAlignChange('left')}
>
<AlignLeft className="h-4 w-4" />
</Button>
<Button
variant={textAlign === 'center' ? 'default' : 'outline'}
size="icon"
onClick={() => handleTextAlignChange('center')}
>
<AlignCenter className="h-4 w-4" />
</Button>
<Button
variant={textAlign === 'right' ? 'default' : 'outline'}
size="icon"
onClick={() => handleTextAlignChange('right')}
>
<AlignRight className="h-4 w-4" />
</Button>
</div>
</div>
<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>
)
}
</div>
</TabsContent>
<TabsContent value="style" className="space-y-4">
<div className="flex items-center justify-between">
<Label>Text Alignment</Label>
<div className="flex space-x-1">
<Button
variant={textAlign === "left" ? "default" : "outline"}
size="icon"
onClick={() => handleTextAlignChange("left")}
>
<AlignLeft className="h-4 w-4" />
</Button>
<Button
variant={textAlign === "center" ? "default" : "outline"}
size="icon"
onClick={() => handleTextAlignChange("center")}
>
<AlignCenter className="h-4 w-4" />
</Button>
<Button
variant={textAlign === "right" ? "default" : "outline"}
size="icon"
onClick={() => handleTextAlignChange("right")}
>
<AlignRight className="h-4 w-4" />
</Button>
</div>
</div>
<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 (
<Card className="p-2">
<CollapsibleComponent text={"Text Control"}>
{content()}
</CollapsibleComponent>
<div className="mt-4 space-y-4">
{
previewText &&
<div className="p-4 border rounded-md overflow-hidden">
<p className="font-bold mb-2">Preview:</p>
<p style={{
fontFamily,
fontSize: `${fontSize}px`,
fontStyle,
fontWeight,
lineHeight,
letterSpacing: `${charSpacing}px`,
textDecoration: `${underline ? 'underline' : ''} ${linethrough ? 'line-through' : ''}`,
textAlign
}} className='truncate'>
{previewText}
</p>
</div>
}
return (
<Card className="p-2">
<CollapsibleComponent text={"Text Control"}>
{content()}
</CollapsibleComponent>
<div className="mt-4 space-y-4">
{previewText && (
<div className="p-4 border rounded-md overflow-hidden">
<p className="font-bold mb-2">Preview:</p>
<p
style={{
fontFamily,
fontSize: `${fontSize}px`,
fontStyle,
fontWeight,
lineHeight,
letterSpacing: `${charSpacing}px`,
textDecoration: `${underline ? "underline" : ""} ${
linethrough ? "line-through" : ""
}`,
textAlign,
}}
className="truncate"
>
{previewText}
</p>
</div>
)}
<Button onClick={applyChanges} className="w-full">
Apply Changes
</Button>
</div>
</Card>
)
}
export default TextCustomization
<Button onClick={applyChanges} className="w-full">
Apply Changes
</Button>
</div>
</Card>
);
};
export default TextCustomization;

View file

@ -1,17 +1,28 @@
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 = [
{ id: "design", icon: Text, label: "Design" },
{ id: "text", icon: Text, label: "Text" },
{ id: "shape", icon: Square, label: "Shape" },
{ id: "upload", icon: Upload, label: "Upload" },
{ id: "text", icon: Type, label: "Text" },
{ id: "shape", icon: Shapes, label: "Shape" },
{ id: "upload", icon: FolderUp, label: "Upload" },
{ id: "icon", icon: Shield, label: "Icon" },
{ 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 (
<div className="w-20 border-r h-screen bg-background flex flex-col items-center py-4 gap-6">
{sidebarItems.map((item) => {
@ -19,10 +30,12 @@ export function Sidebar({ selectedItem, onItemSelect }) {
return (
<button
key={item.id}
onClick={() => onItemSelect(item.id)}
onClick={() => {
setSelectedPanel(item.id);
}}
className={cn(
"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" />

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() {
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 (
<div className="flex-1 h-[88vh] bg-[#F5F0FF] p-8 flex flex-col mt-20">
{/* Main Canvas Area */}
<div className="flex-1 bg-white rounded-3xl shadow-sm mb-4 flex items-center justify-center">
<div className="w-32 h-32 bg-muted rounded-lg" />
</div>
</div>
<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">
<CardContent className="p-0 space-y-2">
<AspectRatio
ratio={getRatioValue(canvasRatio)}
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
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 { Button } from "../ui/button";
import { Input } from "../ui/input";
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import TextPanel from "./TextPanel";
export function EditorPanel({ type }) {
if (!type) return null;
const EditorPanel = () => {
const { selectedPanel } = useContext(CanvasContext);
const panelContent = {
text: (
<>
<h2 className="text-lg font-semibold mb-4">Text</h2>
<Button className="w-full">
<Plus className="mr-2 h-4 w-4" /> Add Text
</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>
</>
),
const renderPanel = () => {
switch (selectedPanel) {
case "text":
return <TextPanel />;
default:
return;
}
};
return (
<div className="w-80 bg-background border-r border-border p-4 overflow-y-auto">
{panelContent[type]}
</div>
<>
{selectedPanel !== "" && (
<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 { Input } from "../ui/Input";
import { X } from "lucide-react";
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() {
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 (
<div className="w-80 h-[calc(100vh-32px)] bg-background rounded-3xl shadow-lg mx-4 my-4">
<ScrollArea className="h-[calc(100vh-32px)] px-4 py-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Text</h2>
<Button variant="ghost" size="icon">
<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
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Text</h2>
<Button variant="ghost" size="icon">
<X className="h-4 w-4" />
</Button>
</div>
<div className="space-y-4">
<div>
<p className="text-sm text-muted-foreground mb-2">
Default text style
</p>
<Input
placeholder="Add a Heading H1"
className="mb-2 rounded-2xl h-12"
<ScrollArea className="h-[calc(100vh-115px)] px-4 py-4">
<Button
className="w-full bg-[#FF2B85] hover:bg-[#FF2B85] text-white rounded-[10px] mb-6 h-12 font-medium text-xl"
onClick={() => {
addText();
}}
>
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
placeholder="Add a Subheading H2"
className="mb-2 rounded-2xl h-12"
<path
d="M5.83333 7.40837C8.45833 6.10004 11.5417 6.10004 14.1667 7.40837"
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
/>
<Input
placeholder="Add Some body text"
className="rounded-2xl h-12"
<path
d="M10 13.5833V6.60828"
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Button>
{activeObject ? (
<div className="space-y-4">
<CommonPanel />
<TextCustomization />
</div>
<div>
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-muted-foreground">Text Design</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>
) : (
<p className="text-sm font-semibold text-center">
No active object found
</p>
)}
</ScrollArea>
</div>
);