Merge branch 'canvas_v1' into 'main'

canvas layout change, customization alignment changed

See merge request planpostai/planpostai_canvas!3
This commit is contained in:
Md. Latiful Kabir 2025-01-12 10:53:43 +00:00
commit 52a3b80478
13 changed files with 482 additions and 351 deletions

View file

@ -8,6 +8,7 @@ import { Card, CardContent } from './ui/card';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { ObjectShortcut } from './ObjectShortcut';
const aspectRatios = [
{ value: "1:1", label: "Square (1:1)" },
@ -28,10 +29,10 @@ const aspectRatios = [
];
export function AspectCanvas() {
const { setLeftPanelOpen, setRightPanelOpen, setOpenPanel, setCaptureOpen, setOpenSetting, openObjectPanel, setOpenObjectPanel, rightPanelOpen } = useContext(OpenContext);
const { setLeftPanelOpen, setRightPanelOpen, setOpenPanel, setCaptureOpen, setOpenSetting, setOpenObjectPanel, rightPanelOpen } = useContext(OpenContext);
const [selectedRatio, setSelectedRatio] = useState("4:3");
const { canvasRef, canvas, setCanvas, fabricCanvasRef, setCanvasHeight, setCanvasWidth } = useContext(CanvasContext);
const { canvasRef, canvas, setCanvas, fabricCanvasRef, setCanvasHeight, setCanvasWidth, setScreenWidth } = useContext(CanvasContext);
useEffect(() => {
import('fabric').then((fabricModule) => {
@ -80,6 +81,8 @@ export function AspectCanvas() {
}
}
setScreenWidth(document.getElementById("root").offsetWidth);
// Handle responsive behavior for panels
if (document.getElementById("root").offsetWidth <= 640) {
setLeftPanelOpen(false);
@ -87,9 +90,9 @@ export function AspectCanvas() {
}
if (document.getElementById("root").offsetWidth > 640) {
setOpenObjectPanel(false);
setOpenSetting(false);
}
};
// Initial setup
updateCanvasSize();
@ -98,18 +101,7 @@ export function AspectCanvas() {
// Cleanup listener on unmount
return () => window.removeEventListener('resize', updateCanvasSize);
}, [
setCanvasWidth,
setCanvasHeight,
selectedRatio,
canvasRef,
canvas,
openObjectPanel,
setLeftPanelOpen,
setOpenObjectPanel,
setRightPanelOpen,
rightPanelOpen
]);
}, [setCanvasWidth, setCanvasHeight, selectedRatio, canvasRef, canvas, setLeftPanelOpen, setOpenObjectPanel, setRightPanelOpen, rightPanelOpen, setScreenWidth, setOpenSetting]);
useEffect(() => {
if (window.fabric) {
@ -138,10 +130,10 @@ export function AspectCanvas() {
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">
<CardContent className="p-0 space-y-4">
<div className='flex w-full flex-wrap items-center justify-between mx-auto gap-2'>
<CardContent className="p-0 space-y-2">
<div className='flex w-full flex-wrap items-center justify-between mx-auto'>
<div className='flex gap-2'>
<div className='flex gap-1'>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@ -155,7 +147,8 @@ export function AspectCanvas() {
</Tooltip>
</TooltipProvider>
<div className="flex justify-between gap-2 items-center">
<div className="flex justify-between gap-1 items-center">
<div className="block xl:hidden lg:hidden md:hidden">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@ -168,6 +161,8 @@ export function AspectCanvas() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@ -180,6 +175,7 @@ export function AspectCanvas() {
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@ -201,12 +197,12 @@ export function AspectCanvas() {
<TooltipTrigger asChild>
<div className="w-full">
<Select onValueChange={handleRatioChange} value={selectedRatio}>
<SelectTrigger className="w-full">
<SelectTrigger className="w-full text-xs font-bold">
<SelectValue placeholder="Select aspect ratio" />
</SelectTrigger>
<SelectContent>
{aspectRatios.map((ratio) => (
<SelectItem key={ratio.value} value={ratio.value}>
<SelectItem key={ratio.value} value={ratio.value} className="text-xs font-bold">
{ratio.label}
</SelectItem>
))}
@ -223,6 +219,10 @@ export function AspectCanvas() {
</div>
<div>
<ObjectShortcut value={"default"} />
</div>
<Separator />
<AspectRatio ratio={getRatioValue(selectedRatio)} 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">

View file

@ -14,7 +14,7 @@ import { ScrollArea } from './ui/scroll-area';
import RndComponent from './Layouts/RndComponent';
const CanvasSetting = () => {
const { canvas, canvasHeight, canvasWidth } = useContext(CanvasContext);
const { canvas, canvasHeight, canvasWidth, screenWidth } = useContext(CanvasContext);
const { setOpenSetting } = useContext(OpenContext);
const bgImgRef = useRef(null);
@ -168,17 +168,18 @@ const CanvasSetting = () => {
maxHeight: 400,
bound: "parent"
}
const content = () => {
return (
<RndComponent value={rndValue}>
<Card className="px-2 py-2">
<CardTitle className="flex items-center flex-wrap justify-between gap-1">Canvas Setting <Button className="rnd-escape" variant="secondary" onClick={() => setOpenSetting(false)}><X /></Button> </CardTitle>
<Separator className="mt-4" />
<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>
@ -215,12 +216,10 @@ const CanvasSetting = () => {
</div>
</div>
<Separator className="mt-4" />
{/* opacity */}
<div className='flex flex-col gap-2 rnd-escape'>
<div className='flex flex-col gap-2 rnd-escape mt-2'>
<Label>
Adjust Opacity:
Background Opacity:
</Label>
<Slider
defaultValue={[1.0]} // Default value, you can set it to 0.0 or another value
@ -274,8 +273,18 @@ const CanvasSetting = () => {
</div>
</ScrollArea>
</Card>
</RndComponent>
)
}
return screenWidth <= 768 ? (
<RndComponent value={rndValue}>
{content()}
</RndComponent>
) : (
<div>
{content()}
</div>
);
}
export default CanvasSetting

View file

@ -1,77 +1,87 @@
import { useContext, useEffect } from 'react'
import { Label } from './ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Input } from './ui/input';
import ColorContext from './Context/colorContext/ColorContext';
import CanvasContext from './Context/canvasContext/CanvasContext';
import { fabric } from 'fabric';
import { useContext, useEffect, useState } from 'react'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import ColorContext from './Context/colorContext/ColorContext'
import CanvasContext from './Context/canvasContext/CanvasContext'
import { fabric } from 'fabric'
const ColorComponent = () => {
const { canvas } = useContext(CanvasContext);
const {
backgroundType,
setBackgroundType,
solidColor,
gradientColors,
setSolidColor,
setGradientColors,
gradientDirection,
setGradientDirection,
setDirectionCoords,
directionCoords,
} = useContext(ColorContext);
const { canvas } = useContext(CanvasContext)
const { backgroundType, setBackgroundType, solidColor, gradientColors, setSolidColor, setGradientColors, gradientDirection, setGradientDirection, setDirectionCoords,
} = useContext(ColorContext)
const applySolidColor = (color) => {
setSolidColor(color);
};
const [previewBackgroundType, setPreviewBackgroundType] = useState(backgroundType)
const [previewSolidColor, setPreviewSolidColor] = useState(solidColor)
const [previewGradientColors, setPreviewGradientColors] = useState(gradientColors)
const [previewGradientDirection, setPreviewGradientDirection] = useState(gradientDirection)
const applyGradient = () => {
const width = canvas?.width || 0;
const height = canvas?.height || 0;
useEffect(() => {
setPreviewBackgroundType(backgroundType)
setPreviewSolidColor(solidColor)
setPreviewGradientColors(gradientColors)
setPreviewGradientDirection(gradientDirection)
}, [backgroundType, solidColor, gradientColors, gradientDirection])
useEffect(() => {
if (previewBackgroundType === "gradient" && canvas) {
const width = canvas.width || 0
const height = canvas.height || 0
// Define coordinates for linear gradient
const linearCoords = {
"top-to-bottom": { x1: 0, y1: 0, x2: 0, y2: height },
"bottom-to-top": { x1: 0, y1: height, x2: 0, y2: 0 },
"left-to-right": { x1: 0, y1: 0, x2: width, y2: 0 },
"right-to-left": { x1: width, y1: 0, x2: 0, y2: 0 },
};
const directionCoords = linearCoords[gradientDirection];
if (directionCoords) {
setDirectionCoords(directionCoords);
} else {
console.error(`Invalid gradient direction: ${gradientDirection}`);
}
};
useEffect(() => {
if (gradientDirection && backgroundType === "gradient") {
applyGradient();
const coords = linearCoords[previewGradientDirection]
if (coords) {
setDirectionCoords(coords)
}
}, [gradientDirection, backgroundType]);
}
}, [previewBackgroundType, previewGradientDirection, canvas, setDirectionCoords])
useEffect(() => {
const object = canvas?.getActiveObject();
if (!object) {
if (canvas && solidColor && backgroundType === "color") {
canvas.backgroundColor = solidColor;
canvas.renderAll();
const applyChanges = () => {
setBackgroundType(previewBackgroundType)
setSolidColor(previewSolidColor)
setGradientColors(previewGradientColors)
setGradientDirection(previewGradientDirection)
applyToCanvas()
}
if (canvas && directionCoords && gradientColors && backgroundType === "gradient") {
const applyToCanvas = () => {
if (!canvas) return
if (previewBackgroundType === "color") {
canvas.backgroundColor = previewSolidColor
} else if (previewBackgroundType === "gradient") {
const width = canvas.width || 0
const height = canvas.height || 0
const linearCoords = {
"top-to-bottom": { x1: 0, y1: 0, x2: 0, y2: height },
"bottom-to-top": { x1: 0, y1: height, x2: 0, y2: 0 },
"left-to-right": { x1: 0, y1: 0, x2: width, y2: 0 },
"right-to-left": { x1: width, y1: 0, x2: 0, y2: 0 },
}
const coords = linearCoords[previewGradientDirection]
if (coords) {
setDirectionCoords(coords)
const gradient = new fabric.Gradient({
type: "linear",
gradientUnits: "pixels",
coords: directionCoords,
coords: coords,
colorStops: [
{ offset: 0, color: gradientColors.color1 },
{ offset: 1, color: gradientColors.color2 },
{ offset: 0, color: previewGradientColors.color1 },
{ offset: 1, color: previewGradientColors.color2 },
],
});
canvas.backgroundColor = gradient;
canvas.renderAll();
})
canvas.backgroundColor = gradient
}
if (canvas && gradientColors && backgroundType === "radial") {
} else if (previewBackgroundType === "radial") {
const gradient = new fabric.Gradient({
type: "radial",
gradientUnits: "pixels",
@ -84,21 +94,41 @@ const ColorComponent = () => {
r2: Math.min(canvas.width, canvas.height) / 2,
},
colorStops: [
{ offset: 0, color: gradientColors.color1 },
{ offset: 1, color: gradientColors.color2 },
{ offset: 0, color: previewGradientColors.color1 },
{ offset: 1, color: previewGradientColors.color2 },
],
});
canvas.backgroundColor = gradient;
canvas.renderAll();
})
canvas.backgroundColor = gradient
}
canvas.renderAll()
}
const getPreviewStyle = () => {
if (previewBackgroundType === "color") {
return { backgroundColor: previewSolidColor }
} else if (previewBackgroundType === "gradient") {
const direction = {
'top-to-bottom': '180deg',
'bottom-to-top': '0deg',
'left-to-right': '90deg',
'right-to-left': '270deg'
}[previewGradientDirection]
return {
backgroundImage: `linear-gradient(${direction}, ${previewGradientColors.color1}, ${previewGradientColors.color2})`
}
} else if (previewBackgroundType === "radial") {
return {
backgroundImage: `radial-gradient(circle, ${previewGradientColors.color1}, ${previewGradientColors.color2})`
}
}
}
}, [gradientColors, directionCoords, solidColor, canvas, backgroundType]);
return (
<div className='flex flex-wrap my-2 gap-2'>
<div className='flex gap-2 flex-wrap mb-auto'>
<div className='flex flex-col gap-4 p-1'>
<div className='flex flex-col gap-2'>
<Label>Background Type:</Label>
<Select value={backgroundType} onValueChange={setBackgroundType}>
<Select value={previewBackgroundType} onValueChange={setPreviewBackgroundType}>
<SelectTrigger>
<SelectValue placeholder="Select background type" />
</SelectTrigger>
@ -110,24 +140,22 @@ const ColorComponent = () => {
</Select>
</div>
{backgroundType === "color" ? (
{previewBackgroundType === "color" ? (
<div className='flex flex-col gap-2'>
<Label>Solid Color:</Label>
<Input
type="color"
value={solidColor}
onChange={(e) => applySolidColor(e.target.value)}
value={previewSolidColor}
onChange={(e) => setPreviewSolidColor(e.target.value)}
/>
</div>
) : backgroundType === "gradient" ? (
) : previewBackgroundType === "gradient" ? (
<div className='grid grid-cols-1 gap-2'>
<div className='flex flex-wrap gap-2'>
<div className='flex flex-col gap-2'>
<Label>Direction:</Label>
<Select
value={gradientDirection}
onValueChange={(value) => {
setGradientDirection(value);
}}
value={previewGradientDirection}
onValueChange={setPreviewGradientDirection}
>
<SelectTrigger>
<SelectValue placeholder="Select direction" />
@ -141,26 +169,26 @@ const ColorComponent = () => {
</Select>
</div>
<div className='flex gap-2 flex-wrap'>
<div className='flex flex-col gap-2'>
<Label>Color 1:</Label>
<Input
type="color"
value={gradientColors.color1}
value={previewGradientColors.color1}
onChange={(e) =>
setGradientColors((prev) => ({
setPreviewGradientColors((prev) => ({
...prev,
color1: e.target.value,
}))}
/>
</div>
<div className='flex flex-wrap gap-2'>
<div className='flex flex-col gap-2'>
<Label>Color 2:</Label>
<Input
type="color"
value={gradientColors.color2}
value={previewGradientColors.color2}
onChange={(e) =>
setGradientColors((prev) => ({
setPreviewGradientColors((prev) => ({
...prev,
color2: e.target.value,
}))}
@ -168,27 +196,27 @@ const ColorComponent = () => {
</div>
</div>
) : (
<div className='flex flex-wrap gap-2'>
<div className='flex gap-2 flex-wrap'>
<div className='grid grid-cols-1 gap-2'>
<div className='flex flex-col gap-2'>
<Label>Color 1:</Label>
<Input
type="color"
value={gradientColors.color1}
value={previewGradientColors.color1}
onChange={(e) =>
setGradientColors((prev) => ({
setPreviewGradientColors((prev) => ({
...prev,
color1: e.target.value,
}))}
/>
</div>
<div className='flex flex-wrap gap-2'>
<div className='flex flex-col gap-2'>
<Label>Color 2:</Label>
<Input
type="color"
value={gradientColors.color2}
value={previewGradientColors.color2}
onChange={(e) =>
setGradientColors((prev) => ({
setPreviewGradientColors((prev) => ({
...prev,
color2: e.target.value,
}))}
@ -196,8 +224,21 @@ const ColorComponent = () => {
</div>
</div>
)}
</div>
);
};
export default ColorComponent;
<div>
<Label>Preview:</Label>
<div
className='w-full h-24 rounded-md mt-2'
style={getPreviewStyle()}
></div>
</div>
<Button onClick={applyChanges}>
Apply Changes
</Button>
</div>
)
}
export default ColorComponent

View file

@ -6,10 +6,11 @@ const CanvasContextProvider = ({ children }) => {
const [canvas, setCanvas] = useState(null);
const [canvasHeight, setCanvasHeight] = useState(0);
const [canvasWidth, setCanvasWidth] = useState(0);
const [screenWidth, setScreenWidth] = useState(0);
const fabricCanvasRef = useRef(null);
return (
<CanvasContext.Provider value={{ canvasRef, canvas, setCanvas, fabricCanvasRef, canvasHeight, setCanvasHeight, canvasWidth, setCanvasWidth }}>
<CanvasContext.Provider value={{ canvasRef, canvas, setCanvas, fabricCanvasRef, canvasHeight, setCanvasHeight, canvasWidth, setCanvasWidth, screenWidth, setScreenWidth }}>
{children}
</CanvasContext.Provider>
)

View file

@ -3,7 +3,7 @@ import ColorContext from './ColorContext'
const ColorContextProvider = ({ children }) => {
const [backgroundType, setBackgroundType] = useState("color"); // 'color' or 'gradient'
const [solidColor, setSolidColor] = useState("#ffffff");
const [solidColor, setSolidColor] = useState("#FFA500");
const [gradientColors, setGradientColors] = useState({
color1: "#ffffff",
color2: "#e26286",

View file

@ -1,10 +1,21 @@
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ChevronUp, ChevronDown } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
const CollapsibleComponent = ({ children, text }) => {
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(null);
useEffect(() => {
// Check if the text prop is "Canvas Setting" and set isOpen to false
if (text === "Canvas Setting") {
setIsOpen(false);
}
else {
setIsOpen(true)
}
}, [text])
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>

View file

@ -114,11 +114,11 @@ const AllIconsPage = () => {
onChange={handleSearch}
className="border p-2 mb-0 w-full"
/>
<Card className="flex items-center justify-center py-1">
<Card className="flex items-center justify-center rounded-none p-1">
<Grid
columnCount={4}
columnWidth={50}
height={380}
height={330}
rowCount={Math.ceil(filtered.length / 4)}
rowHeight={70}
width={240}

View file

@ -1,7 +1,7 @@
import { useCallback, useContext, useState } from "react"
import { useContext, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { BringToFront, CopyPlus, GroupIcon, PencilRuler, Save, Settings, Shapes, SquareX, Store, Trash2, UngroupIcon, Upload, ChevronDown, ChevronUp, MoveIcon, Type, ImageDown, X } from 'lucide-react';
import { PencilRuler, Save, Settings, Shapes, SquareX, Store, Upload, ChevronDown, ChevronUp, Type, ImageDown, X } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
@ -10,6 +10,7 @@ import CanvasContext from './Context/canvasContext/CanvasContext';
import ActiveObjectContext from './Context/activeObject/ObjectContext';
import { fabric } from 'fabric';
import RndComponent from './Layouts/RndComponent';
import { ObjectShortcut } from "./ObjectShortcut";
export function EditPanel() {
const [isCollapsed, setIsCollapsed] = useState(false);
@ -17,108 +18,7 @@ export function EditPanel() {
const { canvas } = useContext(CanvasContext);
const { setActiveObject, activeObject } = useContext(ActiveObjectContext);
const groupSelectedObjects = () => {
const activeObjects = canvas.getActiveObjects(); // Get selected objects
if (activeObjects.length > 1) {
canvas.discardActiveObject();
const group = new fabric.Group(activeObjects, {
left: canvas?.width / 2,
top: canvas?.height / 2,
originX: "center",
originY: "center",
selectable: true, // Allow group selection
subTargetCheck: true, // Allow individual object selection
hasControls: true, // Enable resizing/movement of the group
})
canvas.remove(...activeObjects);
canvas.add(group);
canvas.setActiveObject(group);
setActiveObject(group);
canvas.renderAll();
} else {
console.log("Select at least two objects")
}
};
const ungroupSelectedObjects = () => {
const activeObject = canvas.getActiveObject();
if (activeObject && activeObject.type === "group") {
// Get the group
const group = activeObject;
canvas.discardActiveObject();
// Remove the group from the canvas
canvas.remove(group);
setActiveObject(null);
// Iterate through each object in the group
group._objects.forEach((object) => {
// Calculate the absolute position based on the group's transformation
const objLeft = object.left * group.scaleX + group.left;
const objTop = object.top * group.scaleY + group.top;
// Reset transformations and positions
object.set({
left: objLeft,
top: objTop,
scaleX: object.scaleX * group.scaleX, // Adjust scale based on group
scaleY: object.scaleY * group.scaleY, // Adjust scale based on group
angle: object.angle + group.angle, // Adjust rotation
hasControls: true, // Allow resizing
selectable: true, // Make selectable
group: null, // Remove group reference
originX: "center",
originY: "center",
});
// Update coordinates
object.setCoords();
// Add the object back to the canvas
canvas.add(object);
});
// Clear the selection
canvas.discardActiveObject();
// Render the canvas to reflect changes
canvas.renderAll();
}
};
// Bring Selected Object to Front
const bringToFront = () => {
if (activeObject) {
activeObject.bringToFront();
canvas.renderAll();
}
};
// Remove Selected Element
const removeSelected = useCallback(() => {
const activeObject = canvas?.getActiveObject();
const allObjects = canvas?.getObjects();
const textObjects = allObjects.filter((obj) => obj.type === 'textbox' || obj.type === 'text' ||
obj.type === 'i-text');
if (activeObject) {
canvas.remove(activeObject);
setActiveObject(null);
}
if (activeObject && textObjects?.length === 1) {
canvas.remove(activeObject);
setActiveObject(null);
}
if (activeObject.length > 1) {
canvas.remove(...activeObject);
setActiveObject(null);
}
}, [canvas, setActiveObject]);
const { setActiveObject } = useContext(ActiveObjectContext);
const saveCanvasState = () => {
// Get the JSON representation of all objects
@ -155,17 +55,6 @@ export function EditPanel() {
// loadCanvasState(canvasState);
};
// duplicating current objects
const duplicating = () => {
// Clone the active object to create a true deep copy
activeObject.clone((clonedObject) => {
// Add the cloned object to the canvas
clonedObject.set("left", clonedObject?.left + 30)
canvas.add(clonedObject);
canvas.renderAll();
});
}
// for clear canvas
const clearCanvas = () => {
canvas.clear();
@ -227,57 +116,7 @@ export function EditPanel() {
</TabsList>
<TabsContent value="edit" className="mt-2">
<TooltipProvider>
<div className="grid grid-cols-3 gap-2">
<ActionButton
icon={<GroupIcon className="h-4 w-4" />}
label="Group"
onClick={groupSelectedObjects}
tooltipContent={
<div className="text-sm">
<p className="font-semibold mb-1">Group selected objects</p>
<p>To select multiple objects:</p>
<ol className="list-decimal list-inside mt-1">
<li>Hold down the Shift key</li>
<li>Click and drag with the left mouse button to select objects</li>
<li>Release the Shift key and mouse button</li>
</ol>
<p className="mt-1">Then click this button to group the selected objects.</p>
</div>
}
/>
<ActionButton
icon={<UngroupIcon className="h-4 w-4" />}
label="Ungroup"
onClick={ungroupSelectedObjects}
tooltipContent="Ungroup selected objects"
/>
<ActionButton
icon={<CopyPlus className="h-4 w-4" />}
label="Duplicate"
onClick={duplicating}
tooltipContent="Duplicate selected objects"
/>
<ActionButton
icon={<BringToFront className="h-4 w-4" />}
label="To Front"
onClick={bringToFront}
tooltipContent="Bring selected objects to front"
/>
<ActionButton
icon={<MoveIcon className="h-4 w-4" />}
label="Move"
onClick={() => { }}
tooltipContent="Move selected objects"
/>
<ActionButton
icon={<Trash2 className="h-4 w-4" />}
label="Remove"
onClick={removeSelected}
tooltipContent="Remove selected objects"
/>
</div>
</TooltipProvider>
<ObjectShortcut value={"edit"} />
</TabsContent>
<TabsContent value="add" className="mt-2">
@ -343,12 +182,14 @@ export function EditPanel() {
onClick={saveCanvasState}
tooltipContent="Save current canvas state"
/>
<div className="block xl:hidden lg:hidden md:hidden">
<ActionButton
icon={<Settings className="h-4 w-4" />}
label="Settings"
onClick={() => setOpenSetting(true)}
tooltipContent="Open canvas settings"
/>
</div>
<ActionButton
icon={<SquareX className="h-4 w-4" />}
label="Clear"

View file

@ -10,7 +10,7 @@ const RndComponent = ({ children, value }) => {
x: valueX,
y: valueY,
width: width,
height: 'auto',
height: height,
}}
minWidth={minWidth}
maxWidth={maxWidth}

View file

@ -59,7 +59,7 @@ const SheetLeftPanel = () => {
<Separator className="my-2" />
<TabsContent value="shapes" className="mt-2 h-[530px] overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
<TabsContent value="shapes" className="mt-2 h-[450px] overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
<AddShapes />
</TabsContent>
@ -67,7 +67,7 @@ const SheetLeftPanel = () => {
<AllIconsPage />
</TabsContent>
<TabsContent value="image" className="mt-2 h-[530px] overflow-y-scroll px-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
<TabsContent value="image" className="mt-2 h-[450px] overflow-y-scroll px-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
<UploadImage />
</TabsContent>
</Tabs>

View file

@ -12,6 +12,9 @@ import { X } from "lucide-react";
import { useContext, useEffect, useState } from "react";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import OpenContext from "../Context/openContext/OpenContext";
import CanvasSetting from "../CanvasSetting";
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
import { Card } from "../ui/card";
const SheetRightPanel = () => {
const { rightPanelOpen, setRightPanelOpen } = useContext(OpenContext)
@ -43,7 +46,7 @@ const SheetRightPanel = () => {
return (
<Sheet onOpenChange={handleOpenChange} open={rightPanelOpen} modal={false}>
<SheetContent className="w-[300px] top-[60px] overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
<SheetContent className="w-[300px] top-[60px]">
<SheetHeader>
<SheetTitle className="text-left flex items-center gap-1 flex-wrap justify-between">
<SheetDescription>Edit Customization</SheetDescription>
@ -52,17 +55,31 @@ const SheetRightPanel = () => {
</Button>
</SheetTitle>
<SheetDescription className="text-left">
<SheetDescription className="text-left text-xs">
Customize each shapes, and text as per your choice.
</SheetDescription>
</SheetHeader>
<div className="my-2 mb-14">
<Separator className="my-2" />
<div className="h-[500px] overflow-y-scroll">
<div>
<Card className="p-2 mx-1">
<CollapsibleComponent text={"Canvas Setting"}>
<div className="mt-2">
<CanvasSetting />
</div>
</CollapsibleComponent>
</Card>
</div>
<div className="my-2">
<Separator className="my-2" />
{
open ? <CustomizeShape /> : <p className='text-sm font-semibold'>No active object found</p>
}
</div>
</div>
</SheetContent>
</Sheet>
)

View file

@ -22,7 +22,7 @@ const ObjectPanel = () => {
valueY: 20,
width: 250,
height: 0,
minWidth: 250,
minWidth: 280,
maxWidth: 300,
minHeight: 0,
maxHeight: 500,
@ -31,7 +31,7 @@ const ObjectPanel = () => {
return (
<RndComponent value={rndValue}>
<Card className="w-full shadow-lg">
<Card className="w-full shadow-lg px-1">
<CardHeader className="p-1 mr-12">
<div className="flex items-center justify-between">
<Button className="rnd-escape" variant={"ghost"} size={"sm"} onClick={() => setOpenObjectPanel(false)}><X /></Button>
@ -48,7 +48,7 @@ const ObjectPanel = () => {
</CollapsibleTrigger>
<CollapsibleContent>
<ScrollArea className="h-[500px] xl:h-fit lg:h-fit md:h-fit">
<div className="h-[450px] xl:h-fit lg:h-fit md:h-fit overflow-y-scroll px-1">
<Tabs className="w-full h-fit"
value={tabValue}
onValueChange={(value) => setTabValue(value)} // Sync tab state with context
@ -101,18 +101,18 @@ const ObjectPanel = () => {
{/* All shapes */}
<TabsContent value="shapes" className="mt-2">
<Card className="p-1">
<div>
<CardTitle className="p-1">Shapes </CardTitle>
<Separator className="my-2" />
<ScrollArea className="h-[450px]">
<AddShapes />
</ScrollArea>
</Card>
</div>
</TabsContent>
{/* Upload images */}
<TabsContent value="images" className="mt-2">
<Card className="p-1">
<div>
<CardTitle className="p-1">Upload</CardTitle>
<Separator className="my-2" />
<ScrollArea className="h-[450px]">
@ -120,23 +120,23 @@ const ObjectPanel = () => {
<UploadImage />
</Card>
</ScrollArea>
</Card>
</div>
</TabsContent>
{/* Customization */}
<TabsContent value="customize" className="mt-2">
<Card className="p-1">
<div>
<CardTitle className="p-1">Object customization </CardTitle>
<Separator className="my-2" />
<ScrollArea className="h-[450px]">
<CustomizeShape />
</ScrollArea>
</Card>
</div>
</TabsContent>
</Tabs>
</ScrollArea>
</div>
</CollapsibleContent>

View file

@ -0,0 +1,211 @@
import { useCallback, useContext } from 'react'
import CanvasContext from './Context/canvasContext/CanvasContext';
import ActiveObjectContext from './Context/activeObject/ObjectContext';
import { fabric } from 'fabric';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import { Button } from './ui/button';
import { BringToFront, CopyPlus, GroupIcon, SquareX, Trash2, UngroupIcon } from 'lucide-react';
export const ObjectShortcut = ({ value }) => {
const { canvas } = useContext(CanvasContext);
const { setActiveObject, activeObject } = useContext(ActiveObjectContext);
const groupSelectedObjects = () => {
const activeObjects = canvas.getActiveObjects(); // Get selected objects
if (activeObjects.length > 1) {
canvas.discardActiveObject();
const group = new fabric.Group(activeObjects, {
left: canvas?.width / 2,
top: canvas?.height / 2,
originX: "center",
originY: "center",
selectable: true, // Allow group selection
subTargetCheck: true, // Allow individual object selection
hasControls: true, // Enable resizing/movement of the group
})
canvas.remove(...activeObjects);
canvas.add(group);
canvas.setActiveObject(group);
setActiveObject(group);
canvas.renderAll();
} else {
console.log("Select at least two objects")
}
};
const ungroupSelectedObjects = () => {
const activeObject = canvas.getActiveObject();
if (activeObject && activeObject.type === "group") {
// Get the group
const group = activeObject;
canvas.discardActiveObject();
// Remove the group from the canvas
canvas.remove(group);
setActiveObject(null);
// Iterate through each object in the group
group._objects.forEach((object) => {
// Calculate the absolute position based on the group's transformation
const objLeft = object.left * group.scaleX + group.left;
const objTop = object.top * group.scaleY + group.top;
// Reset transformations and positions
object.set({
left: objLeft,
top: objTop,
scaleX: object.scaleX * group.scaleX, // Adjust scale based on group
scaleY: object.scaleY * group.scaleY, // Adjust scale based on group
angle: object.angle + group.angle, // Adjust rotation
hasControls: true, // Allow resizing
selectable: true, // Make selectable
group: null, // Remove group reference
originX: "center",
originY: "center",
});
// Update coordinates
object.setCoords();
// Add the object back to the canvas
canvas.add(object);
});
// Clear the selection
canvas.discardActiveObject();
// Render the canvas to reflect changes
canvas.renderAll();
}
};
// Bring Selected Object to Front
const bringToFront = () => {
if (activeObject) {
activeObject.bringToFront();
canvas.renderAll();
}
};
// Remove Selected Element
const removeSelected = useCallback(() => {
const activeObject = canvas?.getActiveObject();
const allObjects = canvas?.getObjects();
const textObjects = allObjects.filter((obj) => obj.type === 'textbox' || obj.type === 'text' ||
obj.type === 'i-text');
if (activeObject) {
canvas.remove(activeObject);
setActiveObject(null);
}
if (activeObject && textObjects?.length === 1) {
canvas.remove(activeObject);
setActiveObject(null);
}
if (activeObject.length > 1) {
canvas.remove(...activeObject);
setActiveObject(null);
}
}, [canvas, setActiveObject]);
// duplicating current objects
const duplicating = () => {
// Clone the active object to create a true deep copy
activeObject.clone((clonedObject) => {
// Add the cloned object to the canvas
clonedObject.set("left", clonedObject?.left + 30)
canvas.add(clonedObject);
canvas.renderAll();
});
}
// for clear canvas
const clearCanvas = () => {
canvas.clear();
canvas.renderAll();
setActiveObject(null);
}
return (
<div>
<TooltipProvider>
<div className={`grid grid-cols-3 gap-2 ${value === "default" ? "xl:grid-cols-6 lg:grid-cols-6 md:grid-cols-6" : "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"}`}>
<ActionButton
icon={<GroupIcon className="h-4 w-4" />}
label="Group"
onClick={groupSelectedObjects}
tooltipContent={
<div className="text-sm">
<p className="font-semibold mb-1">Group selected objects</p>
<p>To select multiple objects:</p>
<ol className="list-decimal list-inside mt-1">
<li>Hold down the Shift key</li>
<li>Click and drag with the left mouse button to select objects</li>
<li>Release the Shift key and mouse button</li>
</ol>
<p className="mt-1">Then click this button to group the selected objects.</p>
</div>
}
/>
<ActionButton
icon={<UngroupIcon className="h-4 w-4" />}
label="Ungroup"
onClick={ungroupSelectedObjects}
tooltipContent="Ungroup selected objects"
/>
<ActionButton
icon={<CopyPlus className="h-4 w-4" />}
label="Duplicate"
onClick={duplicating}
tooltipContent="Duplicate selected objects"
/>
<ActionButton
icon={<BringToFront className="h-4 w-4" />}
label="To Front"
onClick={bringToFront}
tooltipContent="Bring selected objects to front"
/>
<ActionButton
icon={<Trash2 className="h-4 w-4" />}
label="Remove"
onClick={removeSelected}
tooltipContent="Remove selected objects"
/>
<ActionButton
icon={<SquareX className="h-4 w-4" />}
label="Clear"
onClick={clearCanvas}
tooltipContent="Clear entire canvas"
/>
</div>
</TooltipProvider>
</div>
)
}
function ActionButton({ icon, label, onClick, tooltipContent }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="md" className="w-full" onClick={onClick}>
<div className="flex items-center gap-1 p-1">
{icon}
<span className="text-[10px] font-bold">{label}</span>
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" className="max-w-xs">
<p>{tooltipContent}</p>
</TooltipContent>
</Tooltip>
)
}