353 lines
16 KiB
JavaScript
353 lines
16 KiB
JavaScript
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
|
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
|
import { useContext, useRef, useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { fabric } from 'fabric';
|
|
import Resizer from "react-image-file-resizer";
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Slider } from '@/components/ui/slider';
|
|
import { HardDriveUpload, ImagePlus, Upload, Crop, RotateCw, FileType, ChevronUp, ChevronDown } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
|
import CollapsibleComponent from './CollapsibleComponent';
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
|
|
const features = [
|
|
{ icon: Upload, title: 'Auto-Resize', description: 'Images larger than 1080px are automatically resized' },
|
|
{ icon: Crop, title: 'Custom Dimensions', description: 'Set your preferred resize dimensions' },
|
|
{ icon: RotateCw, title: 'Quality & Rotation', description: 'Adjust image quality and rotation as needed' },
|
|
{ icon: FileType, title: 'Format Conversion', description: 'Convert between various image formats' },
|
|
]
|
|
|
|
const AddImageIntoShape = () => {
|
|
const { canvas } = useContext(CanvasContext);
|
|
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
|
const [errorMessage, setErrorMessage] = useState("");
|
|
const [width, setWidth] = useState(1080);
|
|
const [height, setHeight] = useState(1080);
|
|
const [quality, setQuality] = useState(100);
|
|
const [rotation, setRotation] = useState("0");
|
|
const [format, setFormat] = useState('JPEG');
|
|
const fileInputRef = useRef(null);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
const handleResize = (file, callback) => {
|
|
Resizer.imageFileResizer(
|
|
file,
|
|
width,
|
|
height,
|
|
format,
|
|
quality,
|
|
parseInt(rotation),
|
|
(resizedFile) => {
|
|
callback(resizedFile); // Pass the resized file to the callback
|
|
},
|
|
'file' // Output type
|
|
);
|
|
};
|
|
|
|
const fileHandler = (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) {
|
|
setErrorMessage("No file selected.");
|
|
return;
|
|
}
|
|
|
|
// Check if the file is an SVG
|
|
if (file.type === "image/svg+xml") {
|
|
// Add SVG directly to canvas without compression
|
|
const blobUrl = URL.createObjectURL(file);
|
|
const imgElement = new Image();
|
|
imgElement.src = blobUrl;
|
|
|
|
imgElement.onload = () => {
|
|
handleImageInsert(imgElement); // Insert the image without resizing
|
|
URL.revokeObjectURL(blobUrl);
|
|
clearFileInput();
|
|
};
|
|
|
|
imgElement.onerror = () => {
|
|
console.error("Failed to load SVG.");
|
|
setErrorMessage("Failed to load SVG.");
|
|
URL.revokeObjectURL(blobUrl);
|
|
clearFileInput();
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Handle raster images (JPEG, PNG, etc.)
|
|
const imgElement = new Image();
|
|
const blobUrl = URL.createObjectURL(file);
|
|
imgElement.src = blobUrl;
|
|
|
|
imgElement.onload = () => {
|
|
// If the width is greater than 1080px, compress the image
|
|
if (imgElement.width > 1080) {
|
|
handleResize(file, (compressedFile) => {
|
|
const compressedBlobUrl = URL.createObjectURL(compressedFile);
|
|
const compressedImg = new Image();
|
|
compressedImg.src = compressedBlobUrl;
|
|
|
|
compressedImg.onload = () => {
|
|
handleImageInsert(compressedImg); // Insert the resized image
|
|
URL.revokeObjectURL(compressedBlobUrl); // Clean up
|
|
clearFileInput();
|
|
};
|
|
|
|
compressedImg.onerror = () => {
|
|
console.error("Failed to load compressed image.");
|
|
setErrorMessage("Failed to load compressed image.");
|
|
URL.revokeObjectURL(compressedBlobUrl);
|
|
clearFileInput();
|
|
};
|
|
});
|
|
} else {
|
|
handleImageInsert(imgElement); // Insert the original image if no resizing needed
|
|
clearFileInput();
|
|
}
|
|
URL.revokeObjectURL(blobUrl); // Clean up
|
|
clearFileInput();
|
|
};
|
|
|
|
imgElement.onerror = () => {
|
|
console.error("Failed to load image.");
|
|
setErrorMessage("Failed to load image.");
|
|
URL.revokeObjectURL(blobUrl);
|
|
clearFileInput();
|
|
};
|
|
};
|
|
|
|
const clearFileInput = () => {
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = "";
|
|
}
|
|
};
|
|
|
|
const handleImageInsert = (img) => {
|
|
if (!activeObject) {
|
|
setErrorMessage("No active object selected!");
|
|
return;
|
|
}
|
|
// Ensure absolute positioning for the clipPath
|
|
activeObject.set({
|
|
isClipPath: true, // Custom property
|
|
absolutePositioned: true,
|
|
});
|
|
|
|
// Calculate scale factors based on clip object size
|
|
let scaleX = activeObject.width / img.width;
|
|
let scaleY = activeObject.height / img.height;
|
|
if (activeObject?.width < 100) {
|
|
scaleX = 0.2;
|
|
}
|
|
|
|
if (activeObject.height < 100) {
|
|
scaleY = 0.2;
|
|
}
|
|
|
|
// Create a fabric image object with scaling and clipPath
|
|
const fabricImage = new fabric.Image(img, {
|
|
scaleX: scaleX,
|
|
scaleY: scaleY,
|
|
left: activeObject.left,
|
|
top: activeObject.top,
|
|
clipPath: activeObject, // Apply clipPath to the image
|
|
originX: activeObject.originX, // Match origin point
|
|
originY: activeObject.originY // Match origin point
|
|
});
|
|
|
|
// Adjust position based on the clipPath's transformations
|
|
fabricImage.set({
|
|
left: activeObject.left,
|
|
top: activeObject.top,
|
|
angle: activeObject.angle // Match rotation if any
|
|
});
|
|
|
|
canvas.add(fabricImage);
|
|
canvas.setActiveObject(fabricImage);
|
|
setActiveObject(fabricImage);
|
|
canvas.renderAll();
|
|
};
|
|
|
|
// canvas.remove(activeObject);
|
|
|
|
const content = () => {
|
|
return (
|
|
<div>
|
|
|
|
{/* <Card className="my-2">
|
|
<CardContent className="p-0">
|
|
<Alert>
|
|
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">Image Insertion <ImagePlus className="h-5 w-5" /></AlertTitle>
|
|
<AlertDescription className="mt-1">
|
|
<p className="mb-1">
|
|
Insert and customize images within shapes. Adjust image position and clipping after insertion.
|
|
</p>
|
|
<h4 className="font-medium mb-1">Key Features:</h4>
|
|
<div className="grid grid-cols-1 gap-2 mt-2">
|
|
{features.map((feature, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-start p-4 bg-white rounded-lg shadow-sm"
|
|
>
|
|
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
|
<div>
|
|
<h3 className="font-semibold mb-1">{feature.title}</h3>
|
|
<p className="text-sm text-gray-600">{feature.description}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</AlertDescription>
|
|
</Alert>
|
|
</CardContent>
|
|
</Card> */}
|
|
|
|
<Card className="my-2">
|
|
<CardContent className="p-0">
|
|
<Alert>
|
|
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">
|
|
Image Insertion <ImagePlus className="h-5 w-5" />
|
|
</AlertTitle>
|
|
<AlertDescription className="mt-1">
|
|
<p className="mb-1">
|
|
Insert and customize images within shapes. Adjust image position and clipping after insertion.
|
|
</p>
|
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
<CollapsibleTrigger asChild>
|
|
<Button variant="outline" className="w-full justify-between mt-2">
|
|
<span>Key Features</span>
|
|
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<div className="grid grid-cols-1 gap-2 mt-2">
|
|
{features.map((feature, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-start p-4 bg-white rounded-lg shadow-sm"
|
|
>
|
|
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
|
<div>
|
|
<h3 className="font-semibold mb-1">{feature.title}</h3>
|
|
<p className="text-sm text-gray-600">{feature.description}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</AlertDescription>
|
|
</Alert>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{errorMessage && (
|
|
<p className="text-red-600 text-sm mt-2">{errorMessage}</p>
|
|
)}
|
|
<div className='flex flex-col w-[100%]'>
|
|
<div className="space-y-1">
|
|
{/* Width Slider */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Width: {width}px</Label>
|
|
<Slider
|
|
min={300}
|
|
value={[width]}
|
|
max={2000}
|
|
step={10}
|
|
onValueChange={(value) => setWidth(value[0])}
|
|
/>
|
|
</div>
|
|
|
|
{/* Height Slider */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Height: {height}px</Label>
|
|
<Slider
|
|
min={300}
|
|
value={[height]}
|
|
max={2000}
|
|
step={10}
|
|
onValueChange={(value) => setHeight(value[0])}
|
|
/>
|
|
</div>
|
|
|
|
{/* Quality Slider */}
|
|
{
|
|
format === "JPEG" &&
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">Quality: {quality}%</Label>
|
|
<Slider
|
|
value={[quality]}
|
|
max={100}
|
|
step={1}
|
|
onValueChange={(value) => setQuality(value[0])}
|
|
/>
|
|
</div>
|
|
}
|
|
|
|
<div className='grid grid-cols-2 gap-1 items-center'>
|
|
{/* Rotation */}
|
|
<div>
|
|
<Label className="text-xs">Rotation: {rotation}°</Label>
|
|
|
|
<Select value={rotation} onValueChange={(value) => setRotation(value)}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select rotation in degree" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0">0</SelectItem>
|
|
<SelectItem value="45">45</SelectItem>
|
|
<SelectItem value="90">90</SelectItem>
|
|
<SelectItem value="180">180</SelectItem>
|
|
<SelectItem value="270">270</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Format Dropdown */}
|
|
<div>
|
|
<Label className="text-xs">Format</Label>
|
|
<Select value={format} onValueChange={(value) => setFormat(value)}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select format" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="JPEG">JPEG</SelectItem>
|
|
<SelectItem value="PNG">PNG</SelectItem>
|
|
<SelectItem value="WEBP">WEBP</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button className="w-fit h-fit flex flex-wrap gap-1 relative py-4 my-4 mx-auto">
|
|
<HardDriveUpload className="cursor-pointer" />
|
|
Upload Image
|
|
<Input
|
|
className="cursor-pointer bg-white text-black absolute top-0 opacity-0"
|
|
type="file"
|
|
accept="image/*"
|
|
ref={fileInputRef}
|
|
onChange={fileHandler} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Card className="flex flex-col p-2">
|
|
<CollapsibleComponent text={"Insert Image"}>
|
|
{content()}
|
|
</CollapsibleComponent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default AddImageIntoShape;
|
|
|
|
|
|
|