added all panel and solved canvas issue when claer and also added zoom functionallity
This commit is contained in:
parent
8e6637f7fb
commit
1cf062f326
26 changed files with 1875 additions and 1074 deletions
|
|
@ -16,3 +16,8 @@
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.\[\&_svg\]\:size-4 svg {
|
||||||
|
width: 1.3rem !important;
|
||||||
|
height: 1.3rem !important;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import EditorPanel from "./components/Panel/EditorPanel";
|
||||||
import CanvasContext from "./components/Context/canvasContext/CanvasContext";
|
import CanvasContext from "./components/Context/canvasContext/CanvasContext";
|
||||||
import Canvas from "./components/Canvas/Canvas";
|
import Canvas from "./components/Canvas/Canvas";
|
||||||
import ActiveObjectContext from "./components/Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "./components/Context/activeObject/ObjectContext";
|
||||||
|
import CanvasCapture from "./components/CanvasCapture";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -100,6 +101,7 @@ function App() {
|
||||||
{activeObject && <TopBar />}
|
{activeObject && <TopBar />}
|
||||||
<ActionButtons />
|
<ActionButtons />
|
||||||
<Canvas />
|
<Canvas />
|
||||||
|
<CanvasCapture />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
||||||
|
import OpenContext from "./Context/openContext/OpenContext";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -28,6 +29,7 @@ const aspectRatios = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ActionButtons() {
|
export function ActionButtons() {
|
||||||
|
const { setCaptureOpen } = useContext(OpenContext);
|
||||||
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
|
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
|
||||||
const handleRatioChange = (newRatio) => {
|
const handleRatioChange = (newRatio) => {
|
||||||
setCanvasRatio(newRatio);
|
setCanvasRatio(newRatio);
|
||||||
|
|
@ -54,7 +56,12 @@ export function ActionButtons() {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-5">
|
<div
|
||||||
|
className="mr-5"
|
||||||
|
onClick={() => {
|
||||||
|
setCaptureOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect, useContext } from "react";
|
import { useEffect, useContext, useState, useRef } from "react";
|
||||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||||
import OpenContext from "../Context/openContext/OpenContext";
|
import OpenContext from "../Context/openContext/OpenContext";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { Card, CardContent } from "../ui/card";
|
import { Card, CardContent } from "../ui/card";
|
||||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const {
|
const {
|
||||||
|
|
@ -26,14 +27,35 @@ export default function Canvas() {
|
||||||
} = useContext(CanvasContext);
|
} = useContext(CanvasContext);
|
||||||
|
|
||||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
const [zoomLevel, setZoomLevel] = useState(100);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const handleZoom = (newZoom) => {
|
||||||
|
const zoom = Math.min(Math.max(newZoom, 0), 100);
|
||||||
|
setZoomLevel(zoom);
|
||||||
|
|
||||||
|
if (canvasRef.current && canvas) {
|
||||||
|
const scale = zoom / 100;
|
||||||
|
|
||||||
|
// Update canvas dimensions
|
||||||
|
const newWidth = canvasRef.current.offsetWidth * scale;
|
||||||
|
const newHeight = canvasRef.current.offsetHeight * scale;
|
||||||
|
|
||||||
|
canvas.setWidth(newWidth);
|
||||||
|
canvas.setHeight(newHeight);
|
||||||
|
setCanvasWidth(newWidth);
|
||||||
|
setCanvasHeight(newHeight);
|
||||||
|
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvas) return; // Ensure canvas is available
|
if (!canvas) return;
|
||||||
|
|
||||||
// Event handler for mouse down
|
|
||||||
const handleMouseDown = (event) => {
|
const handleMouseDown = (event) => {
|
||||||
const target = event.target; // Get the clicked target
|
const target = event.target;
|
||||||
const activeObject = canvas.getActiveObject(); // Get the active object
|
const activeObject = canvas.getActiveObject();
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
if (target.type === "group") {
|
if (target.type === "group") {
|
||||||
|
|
@ -46,20 +68,99 @@ export default function Canvas() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attach the event listener
|
const handleWheel = (event) => {
|
||||||
canvas.on("mouse:down", handleMouseDown);
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
event.preventDefault();
|
||||||
// Cleanup function to remove the event listener
|
const delta = event.deltaY > 0 ? -1 : 1;
|
||||||
return () => {
|
handleZoom(zoomLevel + delta);
|
||||||
canvas.off("mouse:down", handleMouseDown); // Remove the listener on unmount
|
event.stopPropagation();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [canvas, setActiveObject]);
|
|
||||||
|
const handleKeyboard = (event) => {
|
||||||
|
if (
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
(event.key === "=" || event.key === "-")
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
const delta = event.key === "=" ? 1 : -1;
|
||||||
|
handleZoom(zoomLevel + delta);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastDistance = 0;
|
||||||
|
const handleTouchStart = (event) => {
|
||||||
|
if (event.touches.length === 2) {
|
||||||
|
const touch1 = event.touches[0];
|
||||||
|
const touch2 = event.touches[1];
|
||||||
|
lastDistance = Math.hypot(
|
||||||
|
touch2.clientX - touch1.clientX,
|
||||||
|
touch2.clientY - touch1.clientY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (event) => {
|
||||||
|
if (event.touches.length === 2) {
|
||||||
|
event.preventDefault();
|
||||||
|
const touch1 = event.touches[0];
|
||||||
|
const touch2 = event.touches[1];
|
||||||
|
const distance = Math.hypot(
|
||||||
|
touch2.clientX - touch1.clientX,
|
||||||
|
touch2.clientY - touch1.clientY
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastDistance > 0) {
|
||||||
|
const delta = distance - lastDistance;
|
||||||
|
const zoomDelta = delta > 0 ? 1 : -1;
|
||||||
|
handleZoom(zoomLevel + zoomDelta);
|
||||||
|
}
|
||||||
|
lastDistance = distance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
lastDistance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
handleZoom(zoomLevel);
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.on("mouse:down", handleMouseDown);
|
||||||
|
const canvasContainer = document.getElementById("canvas-ref");
|
||||||
|
|
||||||
|
if (canvasContainer) {
|
||||||
|
canvasContainer.addEventListener("wheel", handleWheel, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
canvasContainer.addEventListener("touchstart", handleTouchStart);
|
||||||
|
canvasContainer.addEventListener("touchmove", handleTouchMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
canvasContainer.addEventListener("touchend", handleTouchEnd);
|
||||||
|
window.addEventListener("keydown", handleKeyboard);
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canvas.off("mouse:down", handleMouseDown);
|
||||||
|
if (canvasContainer) {
|
||||||
|
canvasContainer.removeEventListener("wheel", handleWheel);
|
||||||
|
canvasContainer.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
canvasContainer.removeEventListener("touchmove", handleTouchMove);
|
||||||
|
canvasContainer.removeEventListener("touchend", handleTouchEnd);
|
||||||
|
window.removeEventListener("keydown", handleKeyboard);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [canvas, setActiveObject, zoomLevel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import("fabric").then((fabricModule) => {
|
import("fabric").then((fabricModule) => {
|
||||||
window.fabric = fabricModule.fabric;
|
window.fabric = fabricModule.fabric;
|
||||||
});
|
});
|
||||||
}, []);
|
}, [canvasRef, fabricCanvasRef, setCanvas]);
|
||||||
|
|
||||||
const getRatioValue = (ratio) => {
|
const getRatioValue = (ratio) => {
|
||||||
const [width, height] = ratio.split(":").map(Number);
|
const [width, height] = ratio.split(":").map(Number);
|
||||||
|
|
@ -69,42 +170,33 @@ export default function Canvas() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateCanvasSize = () => {
|
const updateCanvasSize = () => {
|
||||||
if (canvasRef.current && canvas) {
|
if (canvasRef.current && canvas) {
|
||||||
// Update canvas dimensions
|
const newWidth = canvasRef.current.offsetWidth;
|
||||||
const newWidth = canvasRef?.current?.offsetWidth;
|
const newHeight = canvasRef.current.offsetHeight;
|
||||||
const newHeight = canvasRef?.current?.offsetHeight;
|
|
||||||
|
|
||||||
canvas.setWidth(newWidth);
|
canvas.setWidth(newWidth);
|
||||||
canvas.setHeight(newHeight);
|
canvas.setHeight(newHeight);
|
||||||
setCanvasWidth(newWidth);
|
setCanvasWidth(newWidth);
|
||||||
setCanvasHeight(newHeight);
|
setCanvasHeight(newHeight);
|
||||||
|
|
||||||
// Adjust the background image to fit the updated canvas size
|
|
||||||
const bgImage = canvas.backgroundImage;
|
const bgImage = canvas.backgroundImage;
|
||||||
if (bgImage) {
|
if (bgImage) {
|
||||||
// Calculate scaling factors for width and height
|
|
||||||
const scaleX = newWidth / bgImage.width;
|
const scaleX = newWidth / bgImage.width;
|
||||||
const scaleY = newHeight / bgImage.height;
|
const scaleY = newHeight / bgImage.height;
|
||||||
|
|
||||||
// Use the larger scale to cover the entire canvas
|
|
||||||
const scale = Math.max(scaleX, scaleY);
|
const scale = Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
// Apply scale and position the image
|
|
||||||
bgImage.scaleX = scale;
|
bgImage.scaleX = scale;
|
||||||
bgImage.scaleY = scale;
|
bgImage.scaleY = scale;
|
||||||
bgImage.left = 0; // Align left
|
bgImage.left = 0;
|
||||||
bgImage.top = 0; // Align top
|
bgImage.top = 0;
|
||||||
|
|
||||||
// Update the background image
|
|
||||||
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
||||||
} else {
|
} else {
|
||||||
// Render the canvas if no background image
|
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setScreenWidth(document.getElementById("root").offsetWidth);
|
setScreenWidth(document.getElementById("root").offsetWidth);
|
||||||
|
|
||||||
// Handle responsive behavior for panels
|
|
||||||
if (document.getElementById("root").offsetWidth <= 640) {
|
if (document.getElementById("root").offsetWidth <= 640) {
|
||||||
setLeftPanelOpen(false);
|
setLeftPanelOpen(false);
|
||||||
setRightPanelOpen(false);
|
setRightPanelOpen(false);
|
||||||
|
|
@ -114,13 +206,9 @@ export default function Canvas() {
|
||||||
setOpenSetting(false);
|
setOpenSetting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Initial setup
|
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
|
|
||||||
// Listen for window resize
|
|
||||||
window.addEventListener("resize", updateCanvasSize);
|
window.addEventListener("resize", updateCanvasSize);
|
||||||
|
|
||||||
// Cleanup listener on unmount
|
|
||||||
return () => window.removeEventListener("resize", updateCanvasSize);
|
return () => window.removeEventListener("resize", updateCanvasSize);
|
||||||
}, [
|
}, [
|
||||||
setCanvasWidth,
|
setCanvasWidth,
|
||||||
|
|
@ -139,44 +227,75 @@ export default function Canvas() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.fabric) {
|
if (window.fabric) {
|
||||||
if (fabricCanvasRef?.current) {
|
if (fabricCanvasRef?.current) {
|
||||||
fabricCanvasRef?.current.dispose();
|
fabricCanvasRef.current.dispose();
|
||||||
}
|
}
|
||||||
// Set styles directly on the canvas element
|
|
||||||
const canvasElement = document.getElementById("fabric-canvas");
|
const canvasElement = document.getElementById("fabric-canvas");
|
||||||
if (canvasElement) {
|
if (canvasElement) {
|
||||||
canvasElement.classList.add("fabric-canvas-container"); // Add the CSS class
|
canvasElement.classList.add("fabric-canvas-container");
|
||||||
}
|
}
|
||||||
|
|
||||||
fabricCanvasRef.current = new window.fabric.Canvas("fabric-canvas", {
|
fabricCanvasRef.current = new window.fabric.Canvas("fabric-canvas", {
|
||||||
width: canvasRef?.current?.offsetWidth,
|
width: canvasRef?.current?.offsetWidth,
|
||||||
height: canvasRef?.current?.offsetWidth,
|
height: canvasRef?.current?.offsetWidth,
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
|
allowTouchScrolling: true,
|
||||||
|
selection: true,
|
||||||
|
preserveObjectStacking: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
setCanvas(fabricCanvasRef?.current);
|
setCanvas(fabricCanvasRef.current);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [canvasRef, fabricCanvasRef, setCanvas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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 ${
|
{/* Zoom Controls */}
|
||||||
activeObject ? "mt-20" : "mt-20"
|
<div className="fixed bottom-4 right-4 flex items-center gap-4 bg-white p-3 rounded-lg shadow-lg z-50">
|
||||||
} mx-auto bg-white pl-5 pb-5 pt-5 border-0 shadow-none`}
|
<span className="text-sm font-medium min-w-[45px]">{zoomLevel}%</span>
|
||||||
>
|
<Slider
|
||||||
<CardContent className="p-0 space-y-2">
|
value={[zoomLevel]}
|
||||||
<AspectRatio
|
onValueChange={(value) => handleZoom(value[0])}
|
||||||
ratio={getRatioValue(canvasRatio)}
|
min={0}
|
||||||
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"
|
max={100}
|
||||||
|
step={1}
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Canvas Container */}
|
||||||
|
<div className="w-full max-w-4xl mx-auto p-4">
|
||||||
|
<Card
|
||||||
|
className={`w-full p-2 rounded-none flex-1 flex flex-col
|
||||||
|
${activeObject ? "mt-20" : "mt-20"}
|
||||||
|
mx-auto pl-5 pb-5 pt-5 border-0 shadow-none bg-transparent
|
||||||
|
${zoomLevel < 100 ? "overflow-hidden" : ""}`}
|
||||||
>
|
>
|
||||||
<div
|
<CardContent
|
||||||
ref={canvasRef}
|
className="p-0 h-full bg-transparent shadow-none"
|
||||||
className="w-full h-full flex items-center justify-center bg-white rounded-md shadow-lg"
|
ref={containerRef}
|
||||||
id="canvas-ref"
|
|
||||||
>
|
>
|
||||||
<canvas id="fabric-canvas" />
|
<AspectRatio
|
||||||
</div>
|
ratio={getRatioValue(canvasRatio)}
|
||||||
</AspectRatio>
|
className="rounded-lg border-0 border-primary/10 transition-all duration-300 ease-in-out"
|
||||||
</CardContent>
|
>
|
||||||
</Card>
|
<div
|
||||||
|
ref={canvasRef}
|
||||||
|
className="w-full h-full flex items-start justify-center rounded-md shadow-none touch-none"
|
||||||
|
id="canvas-ref"
|
||||||
|
style={{
|
||||||
|
touchAction: "none",
|
||||||
|
transform: `scale(${zoomLevel / 100})`,
|
||||||
|
transformOrigin: "50% 0",
|
||||||
|
transition: "transform 0.1s ease-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<canvas id="fabric-canvas" />
|
||||||
|
</div>
|
||||||
|
</AspectRatio>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,290 +1,301 @@
|
||||||
import { useContext, useRef, useState } from 'react'
|
import { useContext, useRef, useState } from "react";
|
||||||
import CanvasContext from './Context/canvasContext/CanvasContext';
|
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
||||||
import { Card, CardTitle } from './ui/card';
|
import { Card, CardTitle } from "./ui/card";
|
||||||
import { Button } from './ui/button';
|
import { Button } from "./ui/button";
|
||||||
import { Trash2, UploadIcon, X } from 'lucide-react';
|
import { Trash2, UploadIcon, X } from "lucide-react";
|
||||||
import { Separator } from './ui/separator';
|
import { Separator } from "./ui/separator";
|
||||||
import ColorComponent from './ColorComponent';
|
import ColorComponent from "./ColorComponent";
|
||||||
import { Label } from './ui/label';
|
import { Label } from "./ui/label";
|
||||||
import { Input } from './ui/input';
|
import { Input } from "./ui/input";
|
||||||
import { Slider } from './ui/slider';
|
import { Slider } from "./ui/slider";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from "fabric";
|
||||||
import OpenContext from './Context/openContext/OpenContext';
|
import OpenContext from "./Context/openContext/OpenContext";
|
||||||
import { ScrollArea } from './ui/scroll-area';
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
import RndComponent from './Layouts/RndComponent';
|
import RndComponent from "./Layouts/RndComponent";
|
||||||
|
|
||||||
const CanvasSetting = () => {
|
const CanvasSetting = () => {
|
||||||
const { canvas, canvasHeight, canvasWidth, screenWidth } = useContext(CanvasContext);
|
const { canvas, canvasHeight, canvasWidth, screenWidth } =
|
||||||
const { setOpenSetting } = useContext(OpenContext);
|
useContext(CanvasContext);
|
||||||
const bgImgRef = useRef(null);
|
const { setOpenSetting } = useContext(OpenContext);
|
||||||
|
const bgImgRef = useRef(null);
|
||||||
|
|
||||||
const [canvasSettings, setCanvasSettings] = useState({
|
const [canvasSettings, setCanvasSettings] = useState({
|
||||||
width: canvas?.width,
|
width: canvas?.width,
|
||||||
height: canvas?.height,
|
height: canvas?.height,
|
||||||
perPixelTargetFind: true, // Enable per-pixel detection globally
|
perPixelTargetFind: true, // Enable per-pixel detection globally
|
||||||
targetFindTolerance: 4, // Adjust for leniency in pixel-perfect detection
|
targetFindTolerance: 4, // Adjust for leniency in pixel-perfect detection
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (key, value) => {
|
const handleChange = (key, value) => {
|
||||||
setCanvasSettings((prevSettings) => ({
|
setCanvasSettings((prevSettings) => ({
|
||||||
...prevSettings,
|
...prevSettings,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update canvas dimensions
|
// Update canvas dimensions
|
||||||
if (key === 'width') {
|
if (key === "width") {
|
||||||
canvas.setWidth(value); // Update canvas width
|
canvas.setWidth(value); // Update canvas width
|
||||||
} else if (key === 'height') {
|
} else if (key === "height") {
|
||||||
canvas.setHeight(value); // Update canvas height
|
canvas.setHeight(value); // Update canvas height
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust the background image to fit the updated canvas
|
|
||||||
const bgImage = canvas.backgroundImage;
|
|
||||||
if (bgImage) {
|
|
||||||
const canvasWidth = canvas.width;
|
|
||||||
const canvasHeight = canvas.height;
|
|
||||||
|
|
||||||
// Calculate scaling factors for width and height
|
|
||||||
const scaleX = canvasWidth / bgImage.width;
|
|
||||||
const scaleY = canvasHeight / bgImage.height;
|
|
||||||
|
|
||||||
// Choose the larger scale to cover the entire canvas
|
|
||||||
const scale = Math.max(scaleX, scaleY);
|
|
||||||
|
|
||||||
// Apply the scale and center the image
|
|
||||||
bgImage.scaleX = scale;
|
|
||||||
bgImage.scaleY = scale;
|
|
||||||
bgImage.left = 0; // Align left
|
|
||||||
bgImage.top = 0; // Align top
|
|
||||||
|
|
||||||
// Mark the background image as needing an update
|
|
||||||
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
|
||||||
} else {
|
|
||||||
canvas.renderAll(); // Render if no background image
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setBackgroundImage = (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
const blobUrl = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
// Create an image element
|
|
||||||
const imgElement = new Image();
|
|
||||||
imgElement.src = blobUrl;
|
|
||||||
|
|
||||||
imgElement.onload = () => {
|
|
||||||
// Create a fabric.Image instance
|
|
||||||
const img = new fabric.Image(imgElement, {
|
|
||||||
scaleX: canvas.width / imgElement.width,
|
|
||||||
scaleY: canvas.height / imgElement.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the background image on the canvas
|
|
||||||
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
|
|
||||||
|
|
||||||
// Revoke the object URL to free memory
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
imgElement.onerror = () => {
|
|
||||||
console.error("Failed to load the image.");
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setBackgroundOverlayImage = (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
const blobUrl = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
// Create an image element
|
|
||||||
const imgElement = new Image();
|
|
||||||
imgElement.src = blobUrl;
|
|
||||||
|
|
||||||
imgElement.onload = () => {
|
|
||||||
const img = new fabric.Image(imgElement, {
|
|
||||||
scaleX: canvas.width / imgElement.width,
|
|
||||||
scaleY: canvas.height / imgElement.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use setOverlayImage method to add the image as an overlay
|
|
||||||
canvas.setOverlayImage(img, () => {
|
|
||||||
canvas.renderAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Revoke the object URL after image is loaded and set
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const adjustBackgroundOpacity = (value) => {
|
|
||||||
if (canvas) {
|
|
||||||
if (canvas.backgroundImage) {
|
|
||||||
// Update the opacity of the background image if it exists
|
|
||||||
canvas.backgroundImage.set("opacity", value);
|
|
||||||
} else if (canvas.overlayImage) {
|
|
||||||
// Update the opacity of the overlay image if it exists
|
|
||||||
canvas.overlayImage.set("opacity", value);
|
|
||||||
} else {
|
|
||||||
console.error("No background or overlay image found on the canvas.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-render the canvas to apply changes
|
|
||||||
canvas.renderAll();
|
|
||||||
} else {
|
|
||||||
console.error("Canvas is not initialized.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeBackgroundImage = () => {
|
|
||||||
if (canvas) {
|
|
||||||
canvas.backgroundImage = null;
|
|
||||||
canvas.renderAll();
|
|
||||||
}
|
|
||||||
if (bgImgRef.current) {
|
|
||||||
bgImgRef.current.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeBackgroundOverlayImage = () => {
|
|
||||||
if (canvas) {
|
|
||||||
canvas.overlayImage = null;
|
|
||||||
canvas.renderAll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rndValue = {
|
|
||||||
valueX: 0,
|
|
||||||
valueY: 20,
|
|
||||||
width: 250,
|
|
||||||
height: 0,
|
|
||||||
minWidth: 250,
|
|
||||||
maxWidth: 300,
|
|
||||||
minHeight: 0,
|
|
||||||
maxHeight: 400,
|
|
||||||
bound: "parent"
|
|
||||||
}
|
|
||||||
const content = () => {
|
|
||||||
return (
|
|
||||||
<Card className="xl:p-0 lg:p-0 md:p-0 p-2">
|
|
||||||
<CardTitle className="flex items-center flex-wrap justify-between gap-1 xl:hidden lg:hidden md:hidden">Canvas Setting <Button className="rnd-escape" variant="secondary" onClick={() => setOpenSetting(false)}><X /></Button> </CardTitle>
|
|
||||||
<Separator className="mt-4 block xl:hidden lg:hidden md:hidden" />
|
|
||||||
<ScrollArea className="h-[400px] xl:h-fit lg:h-fit md:h-fit">
|
|
||||||
<div className='rnd-escape'>
|
|
||||||
|
|
||||||
<ColorComponent />
|
|
||||||
|
|
||||||
<Separator className="mt-2" />
|
|
||||||
|
|
||||||
<div className='flex flex-col my-2 gap-2 rnd-escape'>
|
|
||||||
<div>
|
|
||||||
<Label>Background:</Label>
|
|
||||||
<div className='flex items-center w-fit gap-2 flex-wrap relative'>
|
|
||||||
<Button className="top-0 absolute flex items-center w-[30px]">
|
|
||||||
<UploadIcon className="cursor-pointer" />
|
|
||||||
<Input
|
|
||||||
ref={bgImgRef}
|
|
||||||
className="absolute top-0 opacity-0"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={setBackgroundImage}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="secondary" className="ml-[35px]" onClick={removeBackgroundImage}><Trash2 /></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Background Overlay:</Label>
|
|
||||||
<div className='flex items-center w-fit gap-2 flex-wrap relative'>
|
|
||||||
<Button className="top-0 absolute flex items-center w-[30px]">
|
|
||||||
<UploadIcon className="cursor-pointer" />
|
|
||||||
<Input
|
|
||||||
className="absolute top-0 opacity-0"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={setBackgroundOverlayImage}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="secondary" className="ml-[35px]" onClick={removeBackgroundOverlayImage}><Trash2 /></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* opacity */}
|
|
||||||
<div className='flex flex-col gap-2 rnd-escape mt-2'>
|
|
||||||
<Label>
|
|
||||||
Background Opacity:
|
|
||||||
</Label>
|
|
||||||
<Slider
|
|
||||||
defaultValue={[1.0]} // Default value, you can set it to 0.0 or another value
|
|
||||||
min={0.0}
|
|
||||||
max={1.0}
|
|
||||||
step={0.01} // Step size for fine control
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const newOpacity = value[0]; // Extract slider value
|
|
||||||
adjustBackgroundOpacity(newOpacity); // Adjust Fabric.js background opacity
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="mt-4" />
|
|
||||||
|
|
||||||
{/* canvas size customization (width/height) */}
|
|
||||||
<div className='flex gap-2 my-2'>
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<Label>
|
|
||||||
Width:
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
min={300}
|
|
||||||
type="number"
|
|
||||||
value={canvasSettings.width}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (canvasWidth > parseInt(e.target.value)) {
|
|
||||||
handleChange('width', parseInt(e.target.value, 10));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-col gap-2'>
|
|
||||||
<Label>
|
|
||||||
Height:
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
min={300}
|
|
||||||
type="number"
|
|
||||||
value={canvasSettings.height}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (canvasHeight > parseInt(e.target.value)) {
|
|
||||||
handleChange('height', parseInt(e.target.value, 10));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return screenWidth <= 768 ? (
|
// Adjust the background image to fit the updated canvas
|
||||||
<RndComponent value={rndValue}>
|
const bgImage = canvas.backgroundImage;
|
||||||
{content()}
|
if (bgImage) {
|
||||||
</RndComponent>
|
const canvasWidth = canvas.width;
|
||||||
) : (
|
const canvasHeight = canvas.height;
|
||||||
<div>
|
|
||||||
{content()}
|
// Calculate scaling factors for width and height
|
||||||
</div>
|
const scaleX = canvasWidth / bgImage.width;
|
||||||
|
const scaleY = canvasHeight / bgImage.height;
|
||||||
|
|
||||||
|
// Choose the larger scale to cover the entire canvas
|
||||||
|
const scale = Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
|
// Apply the scale and center the image
|
||||||
|
bgImage.scaleX = scale;
|
||||||
|
bgImage.scaleY = scale;
|
||||||
|
bgImage.left = 0; // Align left
|
||||||
|
bgImage.top = 0; // Align top
|
||||||
|
|
||||||
|
// Mark the background image as needing an update
|
||||||
|
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
||||||
|
} else {
|
||||||
|
canvas.renderAll(); // Render if no background image
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setBackgroundImage = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const blobUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
// Create an image element
|
||||||
|
const imgElement = new Image();
|
||||||
|
imgElement.src = blobUrl;
|
||||||
|
|
||||||
|
imgElement.onload = () => {
|
||||||
|
// Create a fabric.Image instance
|
||||||
|
const img = new fabric.Image(imgElement, {
|
||||||
|
scaleX: canvas.width / imgElement.width,
|
||||||
|
scaleY: canvas.height / imgElement.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the background image on the canvas
|
||||||
|
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
|
||||||
|
|
||||||
|
// Revoke the object URL to free memory
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
imgElement.onerror = () => {
|
||||||
|
console.error("Failed to load the image.");
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setBackgroundOverlayImage = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const blobUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
// Create an image element
|
||||||
|
const imgElement = new Image();
|
||||||
|
imgElement.src = blobUrl;
|
||||||
|
|
||||||
|
imgElement.onload = () => {
|
||||||
|
const img = new fabric.Image(imgElement, {
|
||||||
|
scaleX: canvas.width / imgElement.width,
|
||||||
|
scaleY: canvas.height / imgElement.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use setOverlayImage method to add the image as an overlay
|
||||||
|
canvas.setOverlayImage(img, () => {
|
||||||
|
canvas.renderAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revoke the object URL after image is loaded and set
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjustBackgroundOpacity = (value) => {
|
||||||
|
if (canvas) {
|
||||||
|
if (canvas.backgroundImage) {
|
||||||
|
// Update the opacity of the background image if it exists
|
||||||
|
canvas.backgroundImage.set("opacity", value);
|
||||||
|
} else if (canvas.overlayImage) {
|
||||||
|
// Update the opacity of the overlay image if it exists
|
||||||
|
canvas.overlayImage.set("opacity", value);
|
||||||
|
} else {
|
||||||
|
console.error("No background or overlay image found on the canvas.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render the canvas to apply changes
|
||||||
|
canvas.renderAll();
|
||||||
|
} else {
|
||||||
|
console.error("Canvas is not initialized.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBackgroundImage = () => {
|
||||||
|
if (canvas) {
|
||||||
|
canvas.backgroundImage = null;
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
if (bgImgRef.current) {
|
||||||
|
bgImgRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBackgroundOverlayImage = () => {
|
||||||
|
if (canvas) {
|
||||||
|
canvas.overlayImage = null;
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rndValue = {
|
||||||
|
valueX: 0,
|
||||||
|
valueY: 20,
|
||||||
|
width: 250,
|
||||||
|
height: 0,
|
||||||
|
minWidth: 250,
|
||||||
|
maxWidth: 300,
|
||||||
|
minHeight: 0,
|
||||||
|
maxHeight: 400,
|
||||||
|
bound: "parent",
|
||||||
|
};
|
||||||
|
const content = () => {
|
||||||
|
return (
|
||||||
|
<Card className="xl:p-0 lg:p-0 md:p-0 p-2 border-none shadow-none">
|
||||||
|
<CardTitle className="flex items-center flex-wrap justify-between gap-1 xl:hidden lg:hidden md:hidden">
|
||||||
|
Canvas Setting{" "}
|
||||||
|
<Button
|
||||||
|
className="rnd-escape"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setOpenSetting(false)}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</Button>{" "}
|
||||||
|
</CardTitle>
|
||||||
|
<Separator className="mt-4 block xl:hidden lg:hidden md:hidden" />
|
||||||
|
<ScrollArea className="h-[400px] xl:h-fit lg:h-fit md:h-fit">
|
||||||
|
<div className="rnd-escape">
|
||||||
|
<ColorComponent />
|
||||||
|
|
||||||
|
<Separator className="mt-2" />
|
||||||
|
|
||||||
|
<div className="flex flex-col my-2 gap-2 rnd-escape">
|
||||||
|
<div>
|
||||||
|
<Label>Background:</Label>
|
||||||
|
<div className="flex items-center w-fit gap-2 flex-wrap relative">
|
||||||
|
<Button className="top-0 absolute flex items-center w-[30px]">
|
||||||
|
<UploadIcon className="cursor-pointer" />
|
||||||
|
<Input
|
||||||
|
ref={bgImgRef}
|
||||||
|
className="absolute top-0 opacity-0"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={setBackgroundImage}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="ml-[35px]"
|
||||||
|
onClick={removeBackgroundImage}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Background Overlay:</Label>
|
||||||
|
<div className="flex items-center w-fit gap-2 flex-wrap relative">
|
||||||
|
<Button className="top-0 absolute flex items-center w-[30px]">
|
||||||
|
<UploadIcon className="cursor-pointer" />
|
||||||
|
<Input
|
||||||
|
className="absolute top-0 opacity-0"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={setBackgroundOverlayImage}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="ml-[35px]"
|
||||||
|
onClick={removeBackgroundOverlayImage}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* opacity */}
|
||||||
|
<div className="flex flex-col gap-2 rnd-escape mt-2">
|
||||||
|
<Label>Background Opacity:</Label>
|
||||||
|
<Slider
|
||||||
|
defaultValue={[1.0]} // Default value, you can set it to 0.0 or another value
|
||||||
|
min={0.0}
|
||||||
|
max={1.0}
|
||||||
|
step={0.01} // Step size for fine control
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newOpacity = value[0]; // Extract slider value
|
||||||
|
adjustBackgroundOpacity(newOpacity); // Adjust Fabric.js background opacity
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="mt-4" />
|
||||||
|
|
||||||
|
{/* canvas size customization (width/height) */}
|
||||||
|
<div className="flex gap-2 my-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Width:</Label>
|
||||||
|
<Input
|
||||||
|
min={300}
|
||||||
|
type="number"
|
||||||
|
value={canvasSettings.width}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (canvasWidth > parseInt(e.target.value)) {
|
||||||
|
handleChange("width", parseInt(e.target.value, 10));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Height:</Label>
|
||||||
|
<Input
|
||||||
|
min={300}
|
||||||
|
type="number"
|
||||||
|
value={canvasSettings.height}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (canvasHeight > parseInt(e.target.value)) {
|
||||||
|
handleChange("height", parseInt(e.target.value, 10));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CanvasSetting
|
return screenWidth <= 768 ? (
|
||||||
|
<RndComponent value={rndValue}>{content()}</RndComponent>
|
||||||
|
) : (
|
||||||
|
<div>{content()}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CanvasSetting;
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,69 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { useContext } from 'react'
|
import { useContext } from "react";
|
||||||
import { shapes } from './shapes';
|
import { shapes } from "./shapes";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from "fabric";
|
||||||
|
|
||||||
const CustomShape = () => {
|
const CustomShape = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
const addShape = (each) => {
|
const addShape = (each) => {
|
||||||
// Load the SVG from the imported file
|
// Load the SVG from the imported file
|
||||||
fabric.loadSVGFromURL(each, (objects, options) => {
|
fabric.loadSVGFromURL(each, (objects, options) => {
|
||||||
const svgGroup = fabric.util.groupSVGElements(objects, options);
|
const svgGroup = fabric.util.groupSVGElements(objects, options);
|
||||||
|
|
||||||
// Calculate canvas center
|
// Calculate canvas center
|
||||||
const centerX = canvas.getWidth() / 2;
|
const centerX = canvas.getWidth() / 2;
|
||||||
const centerY = canvas.getHeight() / 2;
|
const centerY = canvas.getHeight() / 2;
|
||||||
|
|
||||||
// Set properties for centering the SVG
|
// Set properties for centering the SVG
|
||||||
svgGroup.set({
|
svgGroup.set({
|
||||||
left: centerX, // Center horizontally
|
left: centerX, // Center horizontally
|
||||||
top: centerY, // Center vertically
|
top: centerY, // Center vertically
|
||||||
originX: 'center', // Set the origin to the center
|
originX: "center", // Set the origin to the center
|
||||||
originY: 'center',
|
originY: "center",
|
||||||
fill: "#f09b0a",
|
fill: "#f09b0a",
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
});
|
strokeWidth: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// Add SVG to the canvas
|
// Add SVG to the canvas
|
||||||
canvas.add(svgGroup);
|
canvas.add(svgGroup);
|
||||||
canvas.setActiveObject(svgGroup);
|
canvas.setActiveObject(svgGroup);
|
||||||
|
|
||||||
// Update the active object state
|
// Update the active object state
|
||||||
setActiveObject(svgGroup);
|
setActiveObject(svgGroup);
|
||||||
|
|
||||||
// Render the canvas
|
// Render the canvas
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-md grid grid-cols-3 gap-2 p-2"
|
||||||
|
onClick={() => addShape()}
|
||||||
|
>
|
||||||
|
{shapes.map((each) => (
|
||||||
<div
|
<div
|
||||||
className="rounded-md grid grid-cols-3 gap-2 p-2"
|
key={each.shape}
|
||||||
onClick={() => addShape()}
|
className="relative aspect-square flex items-center justify-center bg-secondary rounded-md cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
addShape(each.source);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{shapes.map((each) => (
|
<img
|
||||||
<div
|
src={each.source}
|
||||||
key={each.shape}
|
alt={`Shape ${each.shape}`}
|
||||||
className="relative aspect-square flex items-center justify-center bg-secondary rounded-md cursor-pointer"
|
className="max-w-full max-h-full p-1 object-contain hover:bg-gray-200 rounded-md transition-colors duration-200"
|
||||||
onClick={(e) => {
|
/>
|
||||||
e.stopPropagation()
|
|
||||||
addShape(each.source)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={each.source}
|
|
||||||
alt={`Shape ${each.shape}`}
|
|
||||||
className="max-w-full max-h-full p-1 object-contain hover:bg-gray-200 rounded-md transition-colors duration-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default CustomShape
|
export default CustomShape;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { Lock, Unlock } from "lucide-react";
|
import { Lock, Unlock } from "lucide-react";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
const LockObject = () => {
|
const LockObject = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -51,19 +52,22 @@ const LockObject = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shadow-none border-0">
|
<div className="shadow-none border-0">
|
||||||
<Button
|
<a data-tooltip-id="lock">
|
||||||
onClick={toggleLock}
|
<Button
|
||||||
variant="outline"
|
onClick={toggleLock}
|
||||||
size="icon"
|
variant="outline"
|
||||||
disabled={!activeObject}
|
size="icon"
|
||||||
title={isLocked ? "Unlock object" : "Lock object"}
|
disabled={!activeObject}
|
||||||
>
|
title={isLocked ? "Unlock object" : "Lock object"}
|
||||||
{isLocked ? (
|
>
|
||||||
<Unlock className="h-4 w-4" />
|
{isLocked ? (
|
||||||
) : (
|
<Unlock className="h-4 w-4" />
|
||||||
<Lock className="h-4 w-4" />
|
) : (
|
||||||
)}
|
<Lock className="h-4 w-4" />
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
<Tooltip id="lock" content="Lock object" place="bottom" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { BsTransparency } from "react-icons/bs";
|
import { BsTransparency } from "react-icons/bs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
const OpacityCustomization = () => {
|
const OpacityCustomization = () => {
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
|
@ -31,30 +32,35 @@ const OpacityCustomization = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<div>
|
||||||
<PopoverTrigger asChild>
|
<Popover>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
<PopoverTrigger asChild>
|
||||||
<BsTransparency className="h-4 w-4" size={20} />
|
<a data-tooltip-id="opacity-ic">
|
||||||
</Button>
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
</PopoverTrigger>
|
<BsTransparency className="h-4 w-4" size={20} />
|
||||||
<PopoverContent className="w-64 mt-3">
|
</Button>
|
||||||
<div className="grid gap-4">
|
</a>
|
||||||
<div className="flex items-center justify-between">
|
</PopoverTrigger>
|
||||||
<Label className="text-sm font-medium">Opacity</Label>
|
<PopoverContent className="w-64 mt-3">
|
||||||
<span className="text-sm text-gray-500">
|
<div className="grid gap-4">
|
||||||
{Math.round(opacity * 100)}%
|
<div className="flex items-center justify-between">
|
||||||
</span>
|
<Label className="text-sm font-medium">Opacity</Label>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{Math.round(opacity * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[opacity]}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
onValueChange={(value) => adjustBackgroundOpacity(value[0])}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
</PopoverContent>
|
||||||
value={[opacity]}
|
</Popover>
|
||||||
min={0}
|
<Tooltip id="opacity-ic" content="Transparency" place="bottom" />
|
||||||
max={1}
|
</div>
|
||||||
step={0.01}
|
|
||||||
onValueChange={(value) => adjustBackgroundOpacity(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,133 +4,141 @@ import { Card, CardDescription, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import * as lucideIcons from "lucide-react";
|
import * as lucideIcons from "lucide-react";
|
||||||
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from "fabric";
|
||||||
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
const AllIconsPage = () => {
|
const AllIconsPage = () => {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Assume icons is already defined as shown previously, and filtered is created based on the search query
|
// Assume icons is already defined as shown previously, and filtered is created based on the search query
|
||||||
const icons = Object.entries(lucideIcons)?.filter(([name, Icon]) =>
|
const icons = Object.entries(lucideIcons)?.filter(
|
||||||
!name.includes("Icon") && Icon?.$$typeof
|
([name, Icon]) => !name.includes("Icon") && Icon?.$$typeof
|
||||||
);
|
);
|
||||||
|
|
||||||
const filtered = icons.filter(([name, Icon]) =>
|
const filtered = icons.filter(([name, Icon]) =>
|
||||||
name.toLowerCase().includes(search.toLowerCase())
|
name.toLowerCase().includes(search.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
const handleSearch = (e) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIcon = (e) => {
|
const handleIcon = (e) => {
|
||||||
// Check if the target is an SVG or path
|
// Check if the target is an SVG or path
|
||||||
if (e.target.tagName.toLowerCase() === 'svg') {
|
if (e.target.tagName.toLowerCase() === "svg") {
|
||||||
// Serialize the SVG element to a string and pass it
|
// Serialize the SVG element to a string and pass it
|
||||||
const svgString = new XMLSerializer().serializeToString(e.target);
|
const svgString = new XMLSerializer().serializeToString(e.target);
|
||||||
handleAddIcon(svgString);
|
handleAddIcon(svgString);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Invalid Choice",
|
||||||
|
description: "The target is a path element! Select the full icon.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddIcon = (svgString) => {
|
||||||
|
if (!canvas) {
|
||||||
|
console.error("Canvas is not initialized.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!svgString) {
|
||||||
|
console.error("Failed to retrieve SVG string from the icon.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fabric.loadSVGFromString(svgString, (objects, options) => {
|
||||||
|
if (!objects || !options) {
|
||||||
|
console.error("Failed to parse SVG.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively set fill color for all objects
|
||||||
|
const setFillColor = (obj, color) => {
|
||||||
|
if (obj.type === "group" && obj._objects) {
|
||||||
|
obj._objects.forEach((child) => setFillColor(child, color));
|
||||||
} else {
|
} else {
|
||||||
toast({
|
obj.set("stroke", color);
|
||||||
title: "Invalid Choice",
|
|
||||||
description: "The target is a path element! Select the full icon.",
|
|
||||||
variant: "destructive",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddIcon = (svgString) => {
|
objects.forEach((obj) => setFillColor(obj, "#FFA500")); // Set fill color to orange
|
||||||
if (!canvas) {
|
|
||||||
console.error("Canvas is not initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!svgString) {
|
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
||||||
console.error("Failed to retrieve SVG string from the icon.");
|
iconGroup.set({
|
||||||
return;
|
left: canvas.width / 2,
|
||||||
}
|
top: canvas.height / 2,
|
||||||
|
originX: "center",
|
||||||
|
originY: "center",
|
||||||
|
scaleX: 6,
|
||||||
|
scaleY: 6,
|
||||||
|
});
|
||||||
|
|
||||||
fabric.loadSVGFromString(svgString, (objects, options) => {
|
canvas.add(iconGroup);
|
||||||
if (!objects || !options) {
|
canvas.setActiveObject(iconGroup);
|
||||||
console.error("Failed to parse SVG.");
|
setActiveObject(iconGroup);
|
||||||
return;
|
canvas.renderAll();
|
||||||
}
|
});
|
||||||
|
};
|
||||||
// Recursively set fill color for all objects
|
|
||||||
const setFillColor = (obj, color) => {
|
|
||||||
if (obj.type === 'group' && obj._objects) {
|
|
||||||
obj._objects.forEach((child) => setFillColor(child, color));
|
|
||||||
} else {
|
|
||||||
obj.set('stroke', color);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
objects.forEach((obj) => setFillColor(obj, '#FFA500')); // Set fill color to orange
|
|
||||||
|
|
||||||
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
|
||||||
iconGroup.set({
|
|
||||||
left: canvas.width / 2,
|
|
||||||
top: canvas.height / 2,
|
|
||||||
originX: 'center',
|
|
||||||
originY: 'center',
|
|
||||||
scaleX: 6,
|
|
||||||
scaleY: 6,
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.add(iconGroup);
|
|
||||||
canvas.setActiveObject(iconGroup);
|
|
||||||
setActiveObject(iconGroup);
|
|
||||||
canvas.renderAll();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cell component for rendering each icon
|
|
||||||
const Cell = ({ columnIndex, rowIndex, style }) => {
|
|
||||||
const index = rowIndex * 3 + columnIndex; // Adjust columns as needed (3 columns in this case)
|
|
||||||
if (index >= filtered.length) return null; // Handle out-of-bounds index
|
|
||||||
const [name, Icon] = filtered[index];
|
|
||||||
// Define cell-specific styles
|
|
||||||
return (
|
|
||||||
<div style={style}>
|
|
||||||
<div className="bg-red-50 rounded-md ml-1 p-1">
|
|
||||||
<Icon size={32} className="cursor-pointer bg-primary rounded-md text-white p-1" onClick={handleIcon} />
|
|
||||||
<p className="text-xs truncate w-full overflow-hidden whitespace-nowrap">{name}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Cell component for rendering each icon
|
||||||
|
const Cell = ({ columnIndex, rowIndex, style }) => {
|
||||||
|
const index = rowIndex * 3 + columnIndex; // Adjust columns as needed (3 columns in this case)
|
||||||
|
if (index >= filtered.length) return null; // Handle out-of-bounds index
|
||||||
|
const [name, Icon] = filtered[index];
|
||||||
|
// Define cell-specific styles
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-col px-2 py-2 gap-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
|
<div style={style}>
|
||||||
<CardTitle className="flex items-center flex-wrap my-1">All Icons</CardTitle>
|
<div className="bg-red-50 rounded-md ml-1 p-1">
|
||||||
<CardDescription className="text-xs">All copyright (c) for Lucide are held by Lucide Contributors 2022.</CardDescription>
|
<Icon
|
||||||
<Input
|
size={32}
|
||||||
type="text"
|
className="cursor-pointer bg-primary rounded-md text-white p-1 mx-auto"
|
||||||
placeholder="Search icons..."
|
onClick={handleIcon}
|
||||||
onChange={handleSearch}
|
/>
|
||||||
className="border p-2 mb-0 w-full"
|
<p className="text-xs text-center truncate w-full overflow-hidden whitespace-nowrap">
|
||||||
/>
|
{name}
|
||||||
<Card className="flex items-center justify-center rounded-none p-1">
|
</p>
|
||||||
<Grid
|
</div>
|
||||||
columnCount={4}
|
</div>
|
||||||
columnWidth={50}
|
);
|
||||||
height={330}
|
};
|
||||||
rowCount={Math.ceil(filtered.length / 4)}
|
|
||||||
rowHeight={70}
|
return (
|
||||||
width={240}
|
<Card className="flex flex-col py-2 gap-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white border-none shadow-none">
|
||||||
className="scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white"
|
<CardTitle className="flex items-center flex-wrap my-1">
|
||||||
>
|
All Icons
|
||||||
{Cell}
|
</CardTitle>
|
||||||
</Grid>
|
<CardDescription className="text-xs">
|
||||||
</Card>
|
All copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||||
</Card>
|
</CardDescription>
|
||||||
)
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search icons..."
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="border p-2 mb-0 w-[280px]"
|
||||||
|
/>
|
||||||
|
<Card className="flex items-center justify-center rounded-none p-1 border-none shadow-none">
|
||||||
|
<Grid
|
||||||
|
columnCount={3}
|
||||||
|
columnWidth={90}
|
||||||
|
height={330}
|
||||||
|
rowCount={Math.ceil(filtered.length / 3)}
|
||||||
|
rowHeight={70}
|
||||||
|
width={300}
|
||||||
|
className="scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white"
|
||||||
|
>
|
||||||
|
{Cell}
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AllIconsPage;
|
export default AllIconsPage;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,95 @@
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from "react";
|
||||||
import { ArrowBigRight, Diamond, Hexagon, Octagon, Pentagon, Sparkle, Square, Star, Triangle } from 'lucide-react'
|
import {
|
||||||
|
ArrowBigRight,
|
||||||
|
Diamond,
|
||||||
|
Hexagon,
|
||||||
|
Octagon,
|
||||||
|
Pentagon,
|
||||||
|
Sparkle,
|
||||||
|
Square,
|
||||||
|
Star,
|
||||||
|
Triangle,
|
||||||
|
} from "lucide-react";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from "fabric";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from "@/components/ui/card";
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
const RoundedShape = () => {
|
const RoundedShape = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
const shapes = [
|
const shapes = [
|
||||||
{ icon: <ArrowBigRight />, name: 'Arrow' },
|
{ icon: <ArrowBigRight />, name: "Arrow" },
|
||||||
{ icon: <Diamond />, name: 'Diamond' },
|
{ icon: <Diamond />, name: "Diamond" },
|
||||||
{ icon: <Hexagon />, name: 'Hexagon' },
|
{ icon: <Hexagon />, name: "Hexagon" },
|
||||||
{ icon: <Octagon />, name: 'Octagon' },
|
{ icon: <Octagon />, name: "Octagon" },
|
||||||
{ icon: <Pentagon />, name: 'Pentagon' },
|
{ icon: <Pentagon />, name: "Pentagon" },
|
||||||
{ icon: <Sparkle />, name: 'Sparkle' },
|
{ icon: <Sparkle />, name: "Sparkle" },
|
||||||
{ icon: <Square />, name: 'Square' },
|
{ icon: <Square />, name: "Square" },
|
||||||
{ icon: <Star />, name: 'Star' },
|
{ icon: <Star />, name: "Star" },
|
||||||
{ icon: <Triangle />, name: 'Triangle' },
|
{ icon: <Triangle />, name: "Triangle" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const addObject = (icon) => {
|
const addObject = (icon) => {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
console.error("Canvas is not initialized.");
|
console.error("Canvas is not initialized.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const svgString = ReactDOMServer.renderToStaticMarkup(icon);
|
const svgString = ReactDOMServer.renderToStaticMarkup(icon);
|
||||||
|
|
||||||
if (!svgString) {
|
if (!svgString) {
|
||||||
console.error("Failed to retrieve SVG string from icon.");
|
console.error("Failed to retrieve SVG string from icon.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Load SVG onto the Fabric.js canvas
|
// Load SVG onto the Fabric.js canvas
|
||||||
fabric.loadSVGFromString(svgString, (objects, options) => {
|
fabric.loadSVGFromString(svgString, (objects, options) => {
|
||||||
if (!objects || !options) {
|
if (!objects || !options) {
|
||||||
console.error("Failed to parse SVG.");
|
console.error("Failed to parse SVG.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
||||||
iconGroup.set({
|
iconGroup.set({
|
||||||
left: canvas.width / 2,
|
left: canvas.width / 2,
|
||||||
top: canvas.height / 2,
|
top: canvas.height / 2,
|
||||||
originX: 'center',
|
originX: "center",
|
||||||
originY: 'center',
|
originY: "center",
|
||||||
fill: "#f09b0a",
|
fill: "#f09b0a",
|
||||||
scaleX: 6,
|
scaleX: 6,
|
||||||
scaleY: 6,
|
scaleY: 6,
|
||||||
strokeWidth: 0,
|
strokeWidth: 0,
|
||||||
stroke: "#ffffff"
|
stroke: "#ffffff",
|
||||||
});
|
});
|
||||||
canvas.add(iconGroup);
|
canvas.add(iconGroup);
|
||||||
canvas.setActiveObject(iconGroup);
|
canvas.setActiveObject(iconGroup);
|
||||||
setActiveObject(iconGroup)
|
setActiveObject(iconGroup);
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2 bg-gradient-to-br from-white to-gray-100 rounded-xl shadow-lg">
|
<Card className="p-2 border-none shadow-none">
|
||||||
<h2 className="font-semibold text-sm mb-1">Rounded Shapes</h2>
|
<h2 className="font-semibold text-sm mb-1">Rounded Shapes</h2>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
<div className="grid grid-cols-3 gap-y-4 gap-x-2">
|
<div className="grid grid-cols-3 gap-y-4 gap-x-2">
|
||||||
{shapes.map((shape, index) => (
|
{shapes.map((shape, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
|
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
|
||||||
onClick={() => addObject(shape.icon)}
|
onClick={() => addObject(shape.icon)}
|
||||||
>
|
>
|
||||||
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
|
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
|
||||||
{React.cloneElement(shape.icon, { size: 36 })}
|
{React.cloneElement(shape.icon, { size: 36 })}
|
||||||
</div>
|
|
||||||
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
|
||||||
)
|
</button>
|
||||||
}
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default RoundedShape
|
export default RoundedShape;
|
||||||
|
|
|
||||||
|
|
@ -1,174 +1,326 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from "react";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from "fabric";
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from "@/components/ui/card";
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Badge, Circle, Heart, Shield } from 'lucide-react';
|
import { Badge, Circle, Heart, Shield } from "lucide-react";
|
||||||
|
|
||||||
const PlainShapes = () => {
|
const PlainShapes = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
const shapes = [
|
const shapes = [
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" className="lucide lucide-arrow-big-left" fill='orange'>
|
icon: (
|
||||||
<path d="M18 15H12v4L5 12l7-7v4h6v6z" />
|
<svg
|
||||||
</svg>, name: 'Arrow'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="lucide lucide-arrow-big-left"
|
||||||
|
fill="orange"
|
||||||
|
>
|
||||||
|
<path d="M18 15H12v4L5 12l7-7v4h6v6z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Arrow",
|
||||||
|
},
|
||||||
|
|
||||||
{ icon: <Badge />, name: 'Badge' },
|
{ icon: <Badge />, name: "Badge" },
|
||||||
{ icon: <Circle />, name: 'Circle' },
|
{ icon: <Circle />, name: "Circle" },
|
||||||
|
|
||||||
{ icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill='orange' className="lucide lucide-club"><path d="M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z" /></svg>, name: 'Club' },
|
{
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-club"
|
||||||
|
>
|
||||||
|
<path d="M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Club",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#ea580c" strokeWidth="2" strokeLinecap="butt" strokeLinejoin="miter" className="lucide lucide-cross">
|
icon: (
|
||||||
<path d="M4 12h16M12 4v16" />
|
<svg
|
||||||
</svg>, name: 'Cross'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ea580c"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className="lucide lucide-cross"
|
||||||
|
>
|
||||||
|
<path d="M4 12h16M12 4v16" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Cross",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-diamond">
|
icon: (
|
||||||
<path d="M12 2L22 12L12 22L2 12Z" />
|
<svg
|
||||||
</svg>, name: 'Diamond'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-diamond"
|
||||||
|
>
|
||||||
|
<path d="M12 2L22 12L12 22L2 12Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Diamond",
|
||||||
|
},
|
||||||
|
|
||||||
{ icon: <Heart />, name: 'Heart' },
|
{ icon: <Heart />, name: "Heart" },
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-hexagon">
|
icon: (
|
||||||
<path d="M12 2L21 8v8l-9 6-9-6V8L12 2z" />
|
<svg
|
||||||
</svg>, name: 'Hexagon'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-hexagon"
|
||||||
|
>
|
||||||
|
<path d="M12 2L21 8v8l-9 6-9-6V8L12 2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Hexagon",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="white" stroke="#ea580c" strokeWidth="2" strokeLinecap="butt" strokeLinejoin="miter" className="lucide lucide-arrow-right">
|
icon: (
|
||||||
<path d="M5 12h14" />
|
<svg
|
||||||
</svg>, name: 'Line'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="white"
|
||||||
|
stroke="#ea580c"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="butt"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className="lucide lucide-arrow-right"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Line",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" stroke="#ec7e7e" strokeWidth="0" className="lucide lucide-octagon">
|
icon: (
|
||||||
<path d="M4 8 L8 4 H16 L20 8 V16 L16 20 H8 L4 16 Z" />
|
<svg
|
||||||
</svg>, name: 'Octagon'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
stroke="#ec7e7e"
|
||||||
|
strokeWidth="0"
|
||||||
|
className="lucide lucide-octagon"
|
||||||
|
>
|
||||||
|
<path d="M4 8 L8 4 H16 L20 8 V16 L16 20 H8 L4 16 Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Octagon",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-pentagon">
|
icon: (
|
||||||
<path d="M2 11 L12 2 L22 11 L19 21 L5 21 Z" />
|
<svg
|
||||||
</svg>, name: 'Pentagon'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-pentagon"
|
||||||
|
>
|
||||||
|
<path d="M2 11 L12 2 L22 11 L19 21 L5 21 Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Pentagon",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill='orange' className="lucide lucide-rectangle-horizontal">
|
icon: (
|
||||||
<path d="M2 6h20v12H2z" />
|
<svg
|
||||||
</svg>, name: 'Rectangle'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-rectangle-horizontal"
|
||||||
|
>
|
||||||
|
<path d="M2 6h20v12H2z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Rectangle",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-triangle-right">
|
icon: (
|
||||||
<path d="M2 4 L22 20 L2 20 Z" />
|
<svg
|
||||||
</svg>, name: 'Right Triangle'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-triangle-right"
|
||||||
|
>
|
||||||
|
<path d="M2 4 L22 20 L2 20 Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Right Triangle",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: <Shield fill="orange" stroke="0" />,
|
||||||
|
name: "Shield",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <Shield fill="orange" stroke="0" />, name: 'Shield'
|
icon: (
|
||||||
},
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-square"
|
||||||
|
>
|
||||||
|
<path d="M3 3h18v18H3z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Rectangle Square",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-square">
|
icon: (
|
||||||
<path d="M3 3h18v18H3z" />
|
<svg
|
||||||
</svg>, name: 'Rectangle Square'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-star"
|
||||||
|
>
|
||||||
|
<path d="M12 2 L14.85 8.9 L22 10 L16.5 14.5 L18 21 L12 17.5 L6 21 L7.5 14.5 L2 10 L9.15 8.9 Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Star",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-triangle"
|
||||||
|
>
|
||||||
|
<path d="M12 4L4 20h16L12 4Z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Triangle",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-star">
|
icon: (
|
||||||
<path d="M12 2 L14.85 8.9 L22 10 L16.5 14.5 L18 21 L12 17.5 L6 21 L7.5 14.5 L2 10 L9.15 8.9 Z" />
|
<svg
|
||||||
</svg>, name: 'Star'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
},
|
width="36"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="orange"
|
||||||
|
className="lucide lucide-rectangle-vertical scale-125"
|
||||||
|
>
|
||||||
|
<path d="M6 2h12v20H6z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
name: "Rectangle Vertical",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
{
|
const addObject = (icon, name) => {
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-triangle">
|
if (!canvas) {
|
||||||
<path d="M12 4L4 20h16L12 4Z" />
|
console.error("Canvas is not initialized.");
|
||||||
</svg>, name: 'Triangle'
|
return;
|
||||||
},
|
}
|
||||||
|
|
||||||
{
|
const svgString = ReactDOMServer.renderToStaticMarkup(icon);
|
||||||
icon: <svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="orange" className="lucide lucide-rectangle-vertical scale-125">
|
|
||||||
<path d="M6 2h12v20H6z" />
|
|
||||||
</svg>, name: 'Rectangle Vertical'
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
if (!svgString) {
|
||||||
|
console.error("Failed to retrieve SVG string from icon.");
|
||||||
const addObject = (icon, name) => {
|
return;
|
||||||
if (!canvas) {
|
}
|
||||||
console.error("Canvas is not initialized.");
|
// Load SVG onto the Fabric.js canvas
|
||||||
return;
|
fabric.loadSVGFromString(svgString, (objects, options) => {
|
||||||
}
|
if (!objects || !options) {
|
||||||
|
console.error("Failed to parse SVG.");
|
||||||
const svgString = ReactDOMServer.renderToStaticMarkup(icon);
|
return;
|
||||||
|
}
|
||||||
if (!svgString) {
|
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
||||||
console.error("Failed to retrieve SVG string from icon.");
|
iconGroup.set({
|
||||||
return;
|
left: canvas.width / 2,
|
||||||
}
|
top: canvas.height / 2,
|
||||||
// Load SVG onto the Fabric.js canvas
|
originX: "center",
|
||||||
fabric.loadSVGFromString(svgString, (objects, options) => {
|
originY: "center",
|
||||||
if (!objects || !options) {
|
fill: "#f09b0a",
|
||||||
console.error("Failed to parse SVG.");
|
scaleX: 6,
|
||||||
return;
|
scaleY: 6,
|
||||||
}
|
strokeWidth: 0,
|
||||||
const iconGroup = fabric.util.groupSVGElements(objects, options);
|
// rx: 0,
|
||||||
iconGroup.set({
|
// x: 0,
|
||||||
left: canvas.width / 2,
|
// y: 0,
|
||||||
top: canvas.height / 2,
|
});
|
||||||
originX: 'center',
|
if (name === "Line") {
|
||||||
originY: 'center',
|
iconGroup.set({
|
||||||
fill: "#f09b0a",
|
strokeWidth: 2,
|
||||||
scaleX: 6,
|
|
||||||
scaleY: 6,
|
|
||||||
strokeWidth: 0,
|
|
||||||
// rx: 0,
|
|
||||||
// x: 0,
|
|
||||||
// y: 0,
|
|
||||||
});
|
|
||||||
if (name === "Line") {
|
|
||||||
iconGroup.set({
|
|
||||||
strokeWidth: 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
canvas.add(iconGroup);
|
|
||||||
canvas.setActiveObject(iconGroup);
|
|
||||||
setActiveObject(iconGroup)
|
|
||||||
canvas.renderAll();
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
canvas.add(iconGroup);
|
||||||
|
canvas.setActiveObject(iconGroup);
|
||||||
|
setActiveObject(iconGroup);
|
||||||
|
canvas.renderAll();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2 bg-gradient-to-br from-white to-gray-100 rounded-xl shadow-lg">
|
<Card className="p-2 rounded-xl shadow-none border-none">
|
||||||
<h2 className="font-semibold text-sm mb-1">Plain Shapes</h2>
|
<h2 className="font-semibold text-sm mb-1">Plain Shapes</h2>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
<div className="grid grid-cols-3 gap-y-4 gap-x-2">
|
<div className="grid grid-cols-3 gap-y-4 gap-x-2">
|
||||||
{shapes.map((shape, index) => (
|
{shapes.map((shape, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
|
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
|
||||||
onClick={() => addObject(shape.icon, shape.name)}
|
onClick={() => addObject(shape.icon, shape.name)}
|
||||||
>
|
>
|
||||||
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
|
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
|
||||||
{React.cloneElement(shape.icon, { size: 36, fill: "#ea580c" })}
|
{React.cloneElement(shape.icon, { size: 36, fill: "#ea580c" })}
|
||||||
</div>
|
|
||||||
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
|
||||||
)
|
</button>
|
||||||
}
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default PlainShapes
|
export default PlainShapes;
|
||||||
|
|
|
||||||
|
|
@ -1,288 +1,308 @@
|
||||||
import { useContext, useRef, useState } from 'react'
|
import { useContext, useRef, useState } from "react";
|
||||||
import CanvasContext from '../Context/canvasContext/CanvasContext'
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import { fabric } from 'fabric'
|
import { fabric } from "fabric";
|
||||||
import { ImageIcon, Trash2 } from 'lucide-react'
|
import { ImageIcon, Trash2 } from "lucide-react";
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import {
|
||||||
import Resizer from "react-image-file-resizer"
|
Select,
|
||||||
import { Slider } from '@/components/ui/slider'
|
SelectContent,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
SelectItem,
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
SelectTrigger,
|
||||||
import { useDropzone } from 'react-dropzone'
|
SelectValue,
|
||||||
import ImageCustomization from './Customization/ImageCustomization'
|
} from "@/components/ui/select";
|
||||||
import { Separator } from '../ui/separator'
|
import Resizer from "react-image-file-resizer";
|
||||||
import ActiveObjectContext from '../Context/activeObject/ObjectContext'
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
import ImageCustomization from "./Customization/ImageCustomization";
|
||||||
|
import { Separator } from "../ui/separator";
|
||||||
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
|
|
||||||
const UploadImage = () => {
|
const UploadImage = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const [width, setWidth] = useState(1080);
|
const [width, setWidth] = useState(1080);
|
||||||
const [height, setHeight] = useState(1080);
|
const [height, setHeight] = useState(1080);
|
||||||
const [quality, setQuality] = useState(100);
|
const [quality, setQuality] = useState(100);
|
||||||
const [rotation, setRotation] = useState("0");
|
const [rotation, setRotation] = useState("0");
|
||||||
const [format, setFormat] = useState('JPEG');
|
const [format, setFormat] = useState("JPEG");
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
const [preview, setPreview] = useState(null);
|
const [preview, setPreview] = useState(null);
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
accept: {
|
accept: {
|
||||||
'image/*': ['.jpeg', '.png', '.gif', '.jpg', '.webp', '.svg']
|
"image/*": [".jpeg", ".png", ".gif", ".jpg", ".webp", ".svg"],
|
||||||
},
|
},
|
||||||
// maxSize: 5 * 1024 * 1024, // 5MB max file size
|
// maxSize: 5 * 1024 * 1024, // 5MB max file size
|
||||||
multiple: false,
|
multiple: false,
|
||||||
onDrop: (acceptedFiles) => {
|
onDrop: (acceptedFiles) => {
|
||||||
if (!acceptedFiles.length) {
|
if (!acceptedFiles.length) {
|
||||||
console.error("No files were dropped.");
|
console.error("No files were dropped.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedFile = acceptedFiles[0];
|
const selectedFile = acceptedFiles[0];
|
||||||
// Create a preview URL
|
// Create a preview URL
|
||||||
const blobUrl = URL.createObjectURL(selectedFile);
|
const blobUrl = URL.createObjectURL(selectedFile);
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreview(blobUrl);
|
setPreview(blobUrl);
|
||||||
|
|
||||||
if (selectedFile.type === "image/svg+xml") {
|
if (selectedFile.type === "image/svg+xml") {
|
||||||
addImageToCanvas(selectedFile);
|
addImageToCanvas(selectedFile);
|
||||||
URL.revokeObjectURL(blobUrl);
|
URL.revokeObjectURL(blobUrl);
|
||||||
} else {
|
} else {
|
||||||
const imgElement = new Image();
|
const imgElement = new Image();
|
||||||
imgElement.src = blobUrl;
|
imgElement.src = blobUrl;
|
||||||
|
|
||||||
imgElement.onload = () => {
|
imgElement.onload = () => {
|
||||||
if (imgElement.width > 1080) {
|
if (imgElement.width > 1080) {
|
||||||
handleResize(selectedFile, (compressedFile) => {
|
handleResize(selectedFile, (compressedFile) => {
|
||||||
addImageToCanvas(compressedFile);
|
addImageToCanvas(compressedFile);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addImageToCanvas(selectedFile);
|
addImageToCanvas(selectedFile);
|
||||||
}
|
}
|
||||||
URL.revokeObjectURL(blobUrl); // Clean up
|
URL.revokeObjectURL(blobUrl); // Clean up
|
||||||
};
|
};
|
||||||
|
|
||||||
imgElement.onerror = () => {
|
imgElement.onerror = () => {
|
||||||
console.error("Failed to load image.");
|
console.error("Failed to load image.");
|
||||||
URL.revokeObjectURL(blobUrl); // Clean up
|
URL.revokeObjectURL(blobUrl); // Clean up
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeFile = () => {
|
||||||
|
// Revoke the object URL to free up memory
|
||||||
|
if (preview) {
|
||||||
|
URL.revokeObjectURL(preview);
|
||||||
|
}
|
||||||
|
setFile(null);
|
||||||
|
setPreview(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
if (activeObject?.type === "image") {
|
||||||
|
canvas.remove(activeObject);
|
||||||
|
setActiveObject(null);
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = (file, callback) => {
|
||||||
|
Resizer.imageFileResizer(
|
||||||
|
file,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
format,
|
||||||
|
quality,
|
||||||
|
parseInt(rotation),
|
||||||
|
(resizedFile) => {
|
||||||
|
callback(resizedFile);
|
||||||
|
},
|
||||||
|
"file"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addImageToCanvas = (file) => {
|
||||||
|
const blobUrl = URL.createObjectURL(file);
|
||||||
|
fabric.Image.fromURL(blobUrl, (img) => {
|
||||||
|
img.set({
|
||||||
|
left: canvas.width / 4,
|
||||||
|
top: canvas.height / 4,
|
||||||
|
scaleX: 0.5,
|
||||||
|
scaleY: 0.5,
|
||||||
|
});
|
||||||
|
canvas.add(img);
|
||||||
|
canvas.setActiveObject(img);
|
||||||
|
// Update the active object state
|
||||||
|
setActiveObject(img);
|
||||||
|
canvas.renderAll();
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const removeFile = () => {
|
return (
|
||||||
// Revoke the object URL to free up memory
|
<Card className="w-full border-none shadow-none">
|
||||||
if (preview) {
|
<CardHeader className="px-4 py-3">
|
||||||
URL.revokeObjectURL(preview)
|
<CardTitle>Image Upload & Editing</CardTitle>
|
||||||
}
|
<CardDescription>
|
||||||
setFile(null)
|
Upload, resize, and apply effects to your images
|
||||||
setPreview(null)
|
</CardDescription>
|
||||||
if (fileInputRef.current) {
|
</CardHeader>
|
||||||
fileInputRef.current.value = ""
|
<CardContent className="p-2">
|
||||||
}
|
<Tabs defaultValue="upload">
|
||||||
if (activeObject?.type === "image") {
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
canvas.remove(activeObject);
|
<TabsTrigger value="upload">Upload</TabsTrigger>
|
||||||
setActiveObject(null);
|
<TabsTrigger value="effects">Effects</TabsTrigger>
|
||||||
canvas.renderAll();
|
</TabsList>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResize = (file, callback) => {
|
{/* Uploads */}
|
||||||
Resizer.imageFileResizer(
|
<TabsContent value="upload">
|
||||||
file,
|
<div className="space-y-4">
|
||||||
width,
|
<div className="grid grid-cols-2 gap-4">
|
||||||
height,
|
<div className="space-y-2">
|
||||||
format,
|
<Label>Width: {width}px</Label>
|
||||||
quality,
|
<Slider
|
||||||
parseInt(rotation),
|
value={[width]}
|
||||||
(resizedFile) => {
|
max={2000}
|
||||||
callback(resizedFile)
|
min={300}
|
||||||
},
|
step={10}
|
||||||
'file',
|
onValueChange={(value) => setWidth(value[0])}
|
||||||
)
|
/>
|
||||||
}
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Height: {height}px</Label>
|
||||||
|
<Slider
|
||||||
|
value={[height]}
|
||||||
|
max={2000}
|
||||||
|
min={300}
|
||||||
|
step={10}
|
||||||
|
onValueChange={(value) => setHeight(value[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{format === "JPEG" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Quality: {quality}%</Label>
|
||||||
|
<Slider
|
||||||
|
value={[quality]}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onValueChange={(value) => setQuality(value[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
const addImageToCanvas = (file) => {
|
<div className="grid grid-cols-2 gap-4">
|
||||||
const blobUrl = URL.createObjectURL(file)
|
<div className="space-y-2">
|
||||||
fabric.Image.fromURL(blobUrl, (img) => {
|
<Label>Format</Label>
|
||||||
img.set({
|
<Select value={format} onValueChange={setFormat}>
|
||||||
left: canvas.width / 4,
|
<SelectTrigger>
|
||||||
top: canvas.height / 4,
|
<SelectValue />
|
||||||
scaleX: 0.5,
|
</SelectTrigger>
|
||||||
scaleY: 0.5,
|
<SelectContent>
|
||||||
})
|
<SelectItem value="JPEG">JPEG</SelectItem>
|
||||||
canvas.add(img)
|
<SelectItem value="PNG">PNG</SelectItem>
|
||||||
canvas.setActiveObject(img);
|
<SelectItem value="WEBP">WEBP</SelectItem>
|
||||||
// Update the active object state
|
</SelectContent>
|
||||||
setActiveObject(img);
|
</Select>
|
||||||
canvas.renderAll();
|
</div>
|
||||||
URL.revokeObjectURL(blobUrl);
|
<div className="space-y-2">
|
||||||
})
|
<Label>Rotation</Label>
|
||||||
}
|
<Select value={rotation} onValueChange={setRotation}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{/* upload image */}
|
||||||
<Card className="w-full mx-auto">
|
{!preview && (
|
||||||
<CardHeader className="px-4 py-3">
|
<div className="max-w-md mx-auto p-4">
|
||||||
<CardTitle>Image Upload & Editing</CardTitle>
|
<Card>
|
||||||
<CardDescription>Upload, resize, and apply effects to your images</CardDescription>
|
<CardContent className="p-6 space-y-4">
|
||||||
</CardHeader>
|
<div
|
||||||
<CardContent className="p-2">
|
{...getRootProps()}
|
||||||
<Tabs defaultValue="upload">
|
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
isDragActive
|
||||||
<TabsTrigger value="upload">Upload</TabsTrigger>
|
? "border-primary bg-primary/10"
|
||||||
<TabsTrigger value="effects">Effects</TabsTrigger>
|
: "border-gray-300 hover:border-primary"
|
||||||
</TabsList>
|
} ${preview ? "hidden" : ""}`}
|
||||||
|
ref={fileInputRef}
|
||||||
{/* Uploads */}
|
>
|
||||||
<TabsContent value="upload">
|
<input {...getInputProps()} />
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col items-center justify-center space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<ImageIcon
|
||||||
<div className="space-y-2">
|
className={`h-12 w-12 ${
|
||||||
<Label>Width: {width}px</Label>
|
isDragActive ? "text-primary" : "text-gray-400"
|
||||||
<Slider
|
}`}
|
||||||
value={[width]}
|
/>
|
||||||
max={2000}
|
<p className="text-sm text-gray-600">
|
||||||
min={300}
|
{isDragActive
|
||||||
step={10}
|
? "Drop file here"
|
||||||
onValueChange={(value) => setWidth(value[0])}
|
: "Drag 'n' drop an image, or click to select a file"}
|
||||||
/>
|
</p>
|
||||||
</div>
|
<p className="text-xs text-gray-500">
|
||||||
<div className="space-y-2">
|
(Max 5MB, image files only)
|
||||||
<Label>Height: {height}px</Label>
|
</p>
|
||||||
<Slider
|
|
||||||
value={[height]}
|
|
||||||
max={2000}
|
|
||||||
min={300}
|
|
||||||
step={10}
|
|
||||||
onValueChange={(value) => setHeight(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{format === "JPEG" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Quality: {quality}%</Label>
|
|
||||||
<Slider
|
|
||||||
value={[quality]}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
onValueChange={(value) => setQuality(value[0])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Format</Label>
|
|
||||||
<Select value={format} onValueChange={setFormat}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="JPEG">JPEG</SelectItem>
|
|
||||||
<SelectItem value="PNG">PNG</SelectItem>
|
|
||||||
<SelectItem value="WEBP">WEBP</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Rotation</Label>
|
|
||||||
<Select value={rotation} onValueChange={setRotation}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* upload image */}
|
|
||||||
{
|
|
||||||
!preview &&
|
|
||||||
<div className="max-w-md mx-auto p-4">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6 space-y-4">
|
|
||||||
<div
|
|
||||||
{...getRootProps()}
|
|
||||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${isDragActive ? 'border-primary bg-primary/10' : 'border-gray-300 hover:border-primary'} ${preview ? 'hidden' : ''}`}
|
|
||||||
ref={fileInputRef}
|
|
||||||
>
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
<div className="flex flex-col items-center justify-center space-y-4">
|
|
||||||
<ImageIcon
|
|
||||||
className={`h-12 w-12 ${isDragActive ? 'text-primary' : 'text-gray-400'}`}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{isDragActive
|
|
||||||
? 'Drop file here'
|
|
||||||
: 'Drag \'n\' drop an image, or click to select a file'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
(Max 5MB, image files only)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* preview image */}
|
|
||||||
{preview && (
|
|
||||||
<Card className="overflow-y-scroll rounded-none">
|
|
||||||
<CardContent className="mt-2 mb-2">
|
|
||||||
<div className="w-fit aspect-square relative h-[100%]">
|
|
||||||
{
|
|
||||||
file?.type === "image/svg+xml" ? <object
|
|
||||||
data={preview}
|
|
||||||
type="image/svg+xml"
|
|
||||||
className="object-cover rounded-lg"
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
>
|
|
||||||
Your browser does not support SVG, no preview available for SVG.
|
|
||||||
</object> :
|
|
||||||
<img
|
|
||||||
src={preview}
|
|
||||||
alt="Uploaded image"
|
|
||||||
className="object-cover rounded-lg overflow-hidden"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Separator className="my-4" />
|
|
||||||
<div className="grid grid-cols-1 gap-2 items-center pb-4">
|
|
||||||
<p className="text-sm text-gray-600 truncate">{file?.name}</p>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
onClick={removeFile}
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Effects */}
|
{/* preview image */}
|
||||||
<TabsContent value="effects">
|
{preview && (
|
||||||
<ImageCustomization />
|
<Card className="overflow-y-scroll rounded-none">
|
||||||
</TabsContent>
|
<CardContent className="mt-2 mb-2">
|
||||||
</Tabs>
|
<div className="w-fit aspect-square relative h-[100%]">
|
||||||
</CardContent>
|
{file?.type === "image/svg+xml" ? (
|
||||||
</Card>
|
<object
|
||||||
)
|
data={preview}
|
||||||
}
|
type="image/svg+xml"
|
||||||
export default UploadImage
|
className="object-cover rounded-lg"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
>
|
||||||
|
Your browser does not support SVG, no preview
|
||||||
|
available for SVG.
|
||||||
|
</object>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt="Uploaded image"
|
||||||
|
className="object-cover rounded-lg overflow-hidden"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<div className="grid grid-cols-1 gap-2 items-center pb-4">
|
||||||
|
<p className="text-sm text-gray-600 truncate">
|
||||||
|
{file?.name}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={removeFile}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Effects */}
|
||||||
|
<TabsContent value="effects">
|
||||||
|
<ImageCustomization />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default UploadImage;
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
const clearCanvas = () => {
|
const clearCanvas = () => {
|
||||||
canvas.clear();
|
canvas.clear();
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
|
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||||
setActiveObject(null);
|
setActiveObject(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
29
src/components/Panel/CanvasPanel.jsx
Normal file
29
src/components/Panel/CanvasPanel.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import CanvasSetting from "../CanvasSetting";
|
||||||
|
|
||||||
|
const CanvasPanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Canvas Settings</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<CanvasSetting />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CanvasPanel;
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
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 FlipCustomization from "../EachComponent/Customization/FlipCustomization";
|
|
||||||
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
|
|
||||||
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
|
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
|
||||||
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
|
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
|
||||||
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
|
|
||||||
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
|
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
|
||||||
import ApplyColor from "../EachComponent/ApplyColor";
|
import ApplyColor from "../EachComponent/ApplyColor";
|
||||||
|
|
||||||
|
|
@ -23,45 +15,17 @@ const CommonPanel = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<SelectObjectFromGroup />
|
|
||||||
|
|
||||||
{/* Apply fill and background color */}
|
{/* Apply fill and background color */}
|
||||||
{activeObjectType !== "image" && !hasClipPath && !customClipPath && (
|
{activeObjectType !== "image" && !hasClipPath && !customClipPath && (
|
||||||
<ApplyColor />
|
<ApplyColor />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 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={"Flip, Rotate Control"}>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<FlipCustomization />
|
|
||||||
<RotateCustomization />
|
|
||||||
</div>
|
|
||||||
</CollapsibleComponent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Skew Customization */}
|
{/* Skew Customization */}
|
||||||
<SkewCustomization />
|
<SkewCustomization />
|
||||||
|
|
||||||
{/* Scale Objects */}
|
{/* Scale Objects */}
|
||||||
<ScaleObjects />
|
<ScaleObjects />
|
||||||
|
|
||||||
{/* Shadow Customization */}
|
|
||||||
<ShadowCustomization />
|
|
||||||
|
|
||||||
{/* Add image into shape */}
|
{/* Add image into shape */}
|
||||||
<AddImageIntoShape />
|
<AddImageIntoShape />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,16 @@ import { useContext } from "react";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import TextPanel from "./TextPanel";
|
import TextPanel from "./TextPanel";
|
||||||
import ColorPanel from "./ColorPanel";
|
import ColorPanel from "./ColorPanel";
|
||||||
|
import ShapePanel from "./ShapePanel";
|
||||||
|
import IconPanel from "./IconPanel";
|
||||||
|
import UploadPanel from "./UploadPanel";
|
||||||
|
import StrokePanel from "./StrokePanel";
|
||||||
|
import ShadowPanel from "./ShadowPanel";
|
||||||
|
import FlipPanel from "./FlipPanel";
|
||||||
|
import PositionPanel from "./PositionPanel";
|
||||||
|
import ImagePanel from "./ImagePanel";
|
||||||
|
import GroupObjectPanel from "./GroupObjectPanel";
|
||||||
|
import CanvasPanel from "./CanvasPanel";
|
||||||
|
|
||||||
const EditorPanel = () => {
|
const EditorPanel = () => {
|
||||||
const { selectedPanel } = useContext(CanvasContext);
|
const { selectedPanel } = useContext(CanvasContext);
|
||||||
|
|
@ -10,8 +20,28 @@ const EditorPanel = () => {
|
||||||
switch (selectedPanel) {
|
switch (selectedPanel) {
|
||||||
case "text":
|
case "text":
|
||||||
return <TextPanel />;
|
return <TextPanel />;
|
||||||
|
case "shape":
|
||||||
|
return <ShapePanel />;
|
||||||
|
case "icon":
|
||||||
|
return <IconPanel />;
|
||||||
|
case "upload":
|
||||||
|
return <UploadPanel />;
|
||||||
case "color":
|
case "color":
|
||||||
return <ColorPanel />;
|
return <ColorPanel />;
|
||||||
|
case "stroke":
|
||||||
|
return <StrokePanel />;
|
||||||
|
case "shadow":
|
||||||
|
return <ShadowPanel />;
|
||||||
|
case "flip":
|
||||||
|
return <FlipPanel />;
|
||||||
|
case "position":
|
||||||
|
return <PositionPanel />;
|
||||||
|
case "image-insert":
|
||||||
|
return <ImagePanel />;
|
||||||
|
case "group-obj":
|
||||||
|
return <GroupObjectPanel />;
|
||||||
|
case "canvas":
|
||||||
|
return <CanvasPanel />;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
src/components/Panel/FlipPanel.jsx
Normal file
39
src/components/Panel/FlipPanel.jsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { Card } from "../ui/card";
|
||||||
|
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
|
||||||
|
import FlipCustomization from "../EachComponent/Customization/FlipCustomization";
|
||||||
|
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
|
||||||
|
|
||||||
|
const FlipPanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Flip & Rotate</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<Card className="shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Flip, Rotate Control"}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FlipCustomization />
|
||||||
|
<RotateCustomization />
|
||||||
|
</div>
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlipPanel;
|
||||||
36
src/components/Panel/GroupObjectPanel.jsx
Normal file
36
src/components/Panel/GroupObjectPanel.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import SelectObjectFromGroup from "../EachComponent/Customization/SelectObjectFromGroup";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
|
||||||
|
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
|
||||||
|
|
||||||
|
const GroupObjectPanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Group Object</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<SelectObjectFromGroup />
|
||||||
|
{/* Skew Customization */}
|
||||||
|
<SkewCustomization />
|
||||||
|
|
||||||
|
{/* Scale Objects */}
|
||||||
|
<ScaleObjects />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupObjectPanel;
|
||||||
30
src/components/Panel/IconPanel.jsx
Normal file
30
src/components/Panel/IconPanel.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import AllIconsPage from "../EachComponent/Icons/AllIcons";
|
||||||
|
|
||||||
|
const IconPanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Icons</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<AllIconsPage />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconPanel;
|
||||||
29
src/components/Panel/ImagePanel.jsx
Normal file
29
src/components/Panel/ImagePanel.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
|
||||||
|
|
||||||
|
const ImagePanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Image</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<AddImageIntoShape />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImagePanel;
|
||||||
29
src/components/Panel/PositionPanel.jsx
Normal file
29
src/components/Panel/PositionPanel.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import PositionCustomization from "../EachComponent/Customization/PositionCustomization";
|
||||||
|
|
||||||
|
const PositionPanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Position Controller</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<PositionCustomization />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PositionPanel;
|
||||||
29
src/components/Panel/ShadowPanel.jsx
Normal file
29
src/components/Panel/ShadowPanel.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
|
||||||
|
|
||||||
|
const ShadowPanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Shadow Color</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<ShadowCustomization />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShadowPanel;
|
||||||
47
src/components/Panel/ShapePanel.jsx
Normal file
47
src/components/Panel/ShapePanel.jsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { Separator } from "../ui/separator";
|
||||||
|
import CustomShape from "../EachComponent/CustomShape/CustomShape";
|
||||||
|
import RoundedShape from "../EachComponent/RoundedShapes/RoundedShape";
|
||||||
|
import PlainShapes from "../EachComponent/Shapes/PlainShapes";
|
||||||
|
|
||||||
|
const ShapePanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Shape</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<h2 className="font-semibold text-sm">Custom Shapes</h2>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<CustomShape />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<RoundedShape />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<PlainShapes />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShapePanel;
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import ApplyColor from "../EachComponent/ApplyColor";
|
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import StrokeCustomization from "../EachComponent/Customization/StrokeCustomization";
|
||||||
|
|
||||||
const ColorPanel = () => {
|
const StrokePanel = () => {
|
||||||
const { setSelectedPanel } = useContext(CanvasContext);
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
<h2 className="text-lg font-semibold">Color</h2>
|
<h2 className="text-lg font-semibold">Stroke Color</h2>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -19,11 +19,11 @@ const ColorPanel = () => {
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="lg:h-[calc(90vh-190px)] xl:h-[calc(100vh-190px)] px-4 py-4">
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
<ApplyColor />
|
<StrokeCustomization />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ColorPanel;
|
export default StrokePanel;
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,23 @@ import OpacityCustomization from "../EachComponent/Customization/OpacityCustomiz
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { ObjectShortcut } from "../ObjectShortcut";
|
import { ObjectShortcut } from "../ObjectShortcut";
|
||||||
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ImagePlus } from "lucide-react";
|
||||||
|
import { RxBorderWidth } from "react-icons/rx";
|
||||||
|
import { LuFlipVertical } from "react-icons/lu";
|
||||||
|
import { SlTarget } from "react-icons/sl";
|
||||||
|
import { RiShadowLine } from "react-icons/ri";
|
||||||
|
import { FaRegObjectGroup } from "react-icons/fa";
|
||||||
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
export function TopBar() {
|
export function TopBar() {
|
||||||
const { selectedPanel } = useContext(CanvasContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
const { selectedPanel, setSelectedPanel, textColor } =
|
||||||
|
useContext(CanvasContext);
|
||||||
|
const activeObjectType = activeObject?.type;
|
||||||
|
const hasClipPath = !!activeObject?.clipPath;
|
||||||
|
const customClipPath = activeObject?.isClipPath;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
|
|
@ -21,6 +35,147 @@ export function TopBar() {
|
||||||
<div>
|
<div>
|
||||||
<TextCustomization />
|
<TextCustomization />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-4 px-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 border-dashed border-2 rounded-md hover:bg-gray-50"
|
||||||
|
onClick={() => setSelectedPanel("image-insert")}
|
||||||
|
>
|
||||||
|
<ImagePlus className="w-5 h-5" />
|
||||||
|
<span>Add image</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a data-tooltip-id="canvas">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="w-10 h-10"
|
||||||
|
onClick={() => setSelectedPanel("canvas")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="12"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<path d="M14 14 L19 19" stroke="black" strokeWidth="2" />
|
||||||
|
<path
|
||||||
|
d="M15 15 Q16 12, 19 11"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<line
|
||||||
|
x1="3"
|
||||||
|
y1="17"
|
||||||
|
x2="7"
|
||||||
|
y2="21"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="21"
|
||||||
|
y1="17"
|
||||||
|
x2="17"
|
||||||
|
y2="21"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{activeObjectType !== "image" &&
|
||||||
|
activeObject?.type !== "i-text" &&
|
||||||
|
!hasClipPath &&
|
||||||
|
!customClipPath && (
|
||||||
|
<a data-tooltip-id="color-gr">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={"rounded-full"}
|
||||||
|
onClick={() => setSelectedPanel("color")}
|
||||||
|
style={{ backgroundColor: textColor?.fill || "black" }}
|
||||||
|
></Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!customClipPath && (
|
||||||
|
<a data-tooltip-id="stroke">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="w-10 h-10"
|
||||||
|
onClick={() => setSelectedPanel("stroke")}
|
||||||
|
>
|
||||||
|
<RxBorderWidth className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{(activeObject || activeObject.type === "group") && (
|
||||||
|
<a data-tooltip-id="group-obj">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="w-10 h-10"
|
||||||
|
onClick={() => setSelectedPanel("group-obj")}
|
||||||
|
>
|
||||||
|
<FaRegObjectGroup />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-6 w-px bg-gray-200" />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<a data-tooltip-id="flip">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("flip")}
|
||||||
|
>
|
||||||
|
<LuFlipVertical />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{activeObject?.type !== "group" && (
|
||||||
|
<a data-tooltip-id="position">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("position")}
|
||||||
|
>
|
||||||
|
<SlTarget />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<a data-tooltip-id="shadow">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("shadow")}
|
||||||
|
>
|
||||||
|
<RiShadowLine />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<OpacityCustomization />
|
<OpacityCustomization />
|
||||||
<div className="h-4 w-px bg-border mx-2" />
|
<div className="h-4 w-px bg-border mx-2" />
|
||||||
|
|
||||||
|
|
@ -33,6 +188,14 @@ export function TopBar() {
|
||||||
</div>
|
</div>
|
||||||
<ScrollBar orientation="horizontal" />
|
<ScrollBar orientation="horizontal" />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
<Tooltip id="color-gr" content="Color" place="bottom" />
|
||||||
|
<Tooltip id="stroke" content="Stroke" place="bottom" />
|
||||||
|
<Tooltip id="position" content="Object position" place="bottom" />
|
||||||
|
<Tooltip id="shadow" content="Shadow color" place="bottom" />
|
||||||
|
<Tooltip id="flip" content="Object flip" place="bottom" />
|
||||||
|
<Tooltip id="group-obj" content="Group Object" place="bottom" />
|
||||||
|
<Tooltip id="canvas" content="Canvas Settings" place="bottom" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
src/components/Panel/UploadPanel.jsx
Normal file
30
src/components/Panel/UploadPanel.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import UploadImage from "../EachComponent/UploadImage";
|
||||||
|
|
||||||
|
const UploadPanel = () => {
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Upload</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSelectedPanel("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
|
<UploadImage />
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UploadPanel;
|
||||||
Loading…
Add table
Reference in a new issue