added text section design layout
This commit is contained in:
parent
976978aec4
commit
954ac950b0
21 changed files with 2156 additions and 1822 deletions
12
src/App.jsx
12
src/App.jsx
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
// import 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 />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { X } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import DesignPanel from "../Panel/DesignPanel";
|
||||
// import TextPanel from "../Panel/TextPanel";
|
||||
import ShapePanel from "../Panel/ShapePanel";
|
||||
|
||||
const panels = {
|
||||
design: DesignPanel,
|
||||
// text: TextPanel,
|
||||
shape: ShapePanel,
|
||||
// Add other panels as needed
|
||||
};
|
||||
|
||||
export default function RightPanel({ activePanel }) {
|
||||
const PanelComponent = panels[activePanel] || (() => null);
|
||||
|
||||
return (
|
||||
<div className="fixed right-0 top-0 h-screen w-72 border-l bg-background p-4">
|
||||
<div className="flex items-center justify-between border-b pb-4">
|
||||
<h2 className="text-lg font-semibold capitalize">{activePanel}</h2>
|
||||
<Button variant="ghost" size="icon">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<PanelComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,147 @@
|
|||
import { useEffect, useContext } from "react";
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
import OpenContext from "../Context/openContext/OpenContext";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
|
||||
export function Canvas() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
65
src/components/Panel/CommonPanel.jsx
Normal file
65
src/components/Panel/CommonPanel.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useContext } from "react";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import SelectObjectFromGroup from "../EachComponent/Customization/SelectObjectFromGroup";
|
||||
import StrokeCustomization from "../EachComponent/Customization/StrokeCustomization";
|
||||
import PositionCustomization from "../EachComponent/Customization/PositionCustomization";
|
||||
import { Card } from "../ui/card";
|
||||
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
|
||||
import OpacityCustomization from "../EachComponent/Customization/OpacityCustomization";
|
||||
import FlipCustomization from "../EachComponent/Customization/FlipCustomization";
|
||||
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
|
||||
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
|
||||
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
|
||||
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
|
||||
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
|
||||
|
||||
const CommonPanel = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
const activeObject = canvas?.getActiveObject();
|
||||
const customClipPath = activeObject?.isClipPath;
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-5">
|
||||
<SelectObjectFromGroup />
|
||||
|
||||
{/* Apply stroke and stroke color */}
|
||||
{!customClipPath && (
|
||||
<>
|
||||
<StrokeCustomization />
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeObject?.type !== "group" && (
|
||||
<>
|
||||
<PositionCustomization />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Controls for opacity, flip, and rotation */}
|
||||
<Card className="shadow-none border-0">
|
||||
<CollapsibleComponent text={"Opacity, Flip, Rotate Control"}>
|
||||
<div className="space-y-2">
|
||||
<OpacityCustomization />
|
||||
<FlipCustomization />
|
||||
<RotateCustomization />
|
||||
</div>
|
||||
</CollapsibleComponent>
|
||||
</Card>
|
||||
|
||||
{/* Skew Customization */}
|
||||
<SkewCustomization />
|
||||
|
||||
{/* Scale Objects */}
|
||||
<ScaleObjects />
|
||||
|
||||
{/* Shadow Customization */}
|
||||
<ShadowCustomization />
|
||||
|
||||
{/* Add image into shape */}
|
||||
<AddImageIntoShape />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonPanel;
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { Label } from "../ui/label";
|
||||
import { Slider } from "../ui/slider";
|
||||
|
||||
export default function DesignPanel() {
|
||||
return (
|
||||
<div className="space-y-4 pt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Shadow Style</Label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-square rounded-lg bg-muted hover:bg-muted/80"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Offset X</Label>
|
||||
<Slider defaultValue={[0]} max={100} step={1} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Offset Y</Label>
|
||||
<Slider defaultValue={[0]} max={100} step={1} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Blur</Label>
|
||||
<Slider defaultValue={[0]} max={100} step={1} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Opacity</Label>
|
||||
<Slider defaultValue={[100]} max={100} step={1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,56 +1,28 @@
|
|||
import { Plus, Search, Upload } from "lucide-react";
|
||||
import { 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;
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import { Input } from "../ui/input";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
|
||||
export default function ShapePanel() {
|
||||
return (
|
||||
<div className="space-y-4 pt-4">
|
||||
<Input placeholder="Search with project name" />
|
||||
<ScrollArea className="h-[calc(100vh-8rem)]">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{Array.from({ length: 9 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="aspect-square rounded-md bg-muted hover:bg-muted/80"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,56 +1,92 @@
|
|||
import { Button } from "../ui/Button";
|
||||
import { 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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue