2024-11-07 19:19:02 +00:00
|
|
|
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
|
2024-11-02 22:38:33 +00:00
|
|
|
import DragableBox from "./DragableBox";
|
2024-11-07 19:19:02 +00:00
|
|
|
import { Coordinates } from "~/types";
|
2024-11-15 21:34:20 +00:00
|
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
import { useSearchParams } from "@remix-run/react";
|
2024-11-02 22:38:33 +00:00
|
|
|
|
2024-11-07 19:19:02 +00:00
|
|
|
type ContainerDimensions = {
|
|
|
|
height: number;
|
|
|
|
width: number;
|
2024-11-15 21:34:20 +00:00
|
|
|
};
|
|
|
|
|
2024-11-16 21:52:33 +00:00
|
|
|
type FileDimensions = {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
};
|
|
|
|
|
2024-11-15 21:34:20 +00:00
|
|
|
type UnitType = "pixels" | "percentage";
|
2024-11-03 17:34:21 +00:00
|
|
|
|
2024-11-16 21:52:33 +00:00
|
|
|
interface ComponentProps {
|
|
|
|
image?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default function CropSelector({ image }: ComponentProps) {
|
2024-11-03 17:34:21 +00:00
|
|
|
const [firstDragable, setFirstDragable] = useState<Coordinates>({
|
2024-11-02 22:38:33 +00:00
|
|
|
x: 0,
|
2024-11-15 21:34:20 +00:00
|
|
|
y: 0,
|
2024-11-03 21:39:34 +00:00
|
|
|
});
|
2024-11-03 17:34:21 +00:00
|
|
|
const [secondDragable, setSecondDragable] = useState<Coordinates>({
|
2024-11-03 21:39:34 +00:00
|
|
|
x: 100,
|
2024-11-15 21:34:20 +00:00
|
|
|
y: 100,
|
2024-11-03 21:39:34 +00:00
|
|
|
});
|
|
|
|
const [selectorPosition, setSelectorPosition] = useState<Coordinates>({
|
2024-11-02 22:38:33 +00:00
|
|
|
x: 0,
|
2024-11-15 21:34:20 +00:00
|
|
|
y: 0,
|
2024-11-03 21:39:34 +00:00
|
|
|
});
|
2024-11-07 19:19:02 +00:00
|
|
|
const [containerSize, setContainerSize] = useState<ContainerDimensions>({
|
|
|
|
height: 0,
|
2024-11-15 21:34:20 +00:00
|
|
|
width: 0,
|
2024-11-07 19:19:02 +00:00
|
|
|
});
|
2024-11-15 21:34:20 +00:00
|
|
|
const [pixelDimensions, setPixelDimensions] = useState<ContainerDimensions>({
|
2024-11-07 19:19:02 +00:00
|
|
|
height: 100,
|
2024-11-15 21:34:20 +00:00
|
|
|
width: 100,
|
2024-11-07 19:19:02 +00:00
|
|
|
});
|
2024-11-15 21:34:20 +00:00
|
|
|
const [unit, setUnit] = useState<UnitType>("pixels");
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
2024-11-03 21:39:34 +00:00
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
2024-11-03 20:56:34 +00:00
|
|
|
|
2024-11-02 22:38:33 +00:00
|
|
|
useEffect(() => {
|
2024-11-03 21:39:34 +00:00
|
|
|
setSelectorPosition({
|
|
|
|
x: Math.min(firstDragable.x, secondDragable.x),
|
2024-11-15 21:34:20 +00:00
|
|
|
y: Math.min(firstDragable.y, secondDragable.y),
|
2024-11-03 21:39:34 +00:00
|
|
|
});
|
2024-11-15 21:34:20 +00:00
|
|
|
const setUrlParams = setTimeout(() => {
|
|
|
|
writeDimensionsToURL();
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
return () => clearTimeout(setUrlParams);
|
2024-11-03 21:39:34 +00:00
|
|
|
}, [firstDragable, secondDragable]);
|
2024-11-03 20:56:34 +00:00
|
|
|
|
2024-11-07 19:19:02 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (!containerRef.current) return;
|
|
|
|
|
2024-11-15 21:34:20 +00:00
|
|
|
const resizeObserver = new ResizeObserver((entries) => {
|
2024-11-07 19:19:02 +00:00
|
|
|
const entry = entries[0];
|
|
|
|
setContainerSize({
|
|
|
|
width: entry.contentRect.width,
|
2024-11-15 21:34:20 +00:00
|
|
|
height: entry.contentRect.height,
|
2024-11-07 19:19:02 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
resizeObserver.observe(containerRef.current);
|
|
|
|
|
|
|
|
return () => resizeObserver.disconnect();
|
|
|
|
}, [containerRef]);
|
|
|
|
|
2024-11-03 21:39:34 +00:00
|
|
|
const selectorWidth = Math.abs(secondDragable.x - firstDragable.x);
|
|
|
|
const selectorHeight = Math.abs(secondDragable.y - firstDragable.y);
|
2024-11-03 20:56:34 +00:00
|
|
|
|
2024-11-03 21:39:34 +00:00
|
|
|
const handleSelectorDrag = (newPosition: Coordinates) => {
|
|
|
|
const deltaX = newPosition.x - selectorPosition.x;
|
|
|
|
const deltaY = newPosition.y - selectorPosition.y;
|
2024-11-02 22:38:33 +00:00
|
|
|
|
2024-11-07 19:19:02 +00:00
|
|
|
const newFirstX = Math.min(firstDragable.x, secondDragable.x) + deltaX;
|
|
|
|
const newFirstY = Math.min(firstDragable.y, secondDragable.y) + deltaY;
|
|
|
|
const newSecondX = Math.max(firstDragable.x, secondDragable.x) + deltaX;
|
|
|
|
const newSecondY = Math.max(firstDragable.y, secondDragable.y) + deltaY;
|
2024-11-03 17:34:21 +00:00
|
|
|
|
2024-11-15 21:34:20 +00:00
|
|
|
if (newFirstX >= 0 && newSecondX <= 100 && newFirstY >= 0 && newSecondY <= 100) {
|
2024-11-07 19:19:02 +00:00
|
|
|
setFirstDragable({
|
|
|
|
x: newFirstX,
|
2024-11-15 21:34:20 +00:00
|
|
|
y: newFirstY,
|
2024-11-07 19:19:02 +00:00
|
|
|
});
|
|
|
|
setSecondDragable({
|
|
|
|
x: newSecondX,
|
2024-11-15 21:34:20 +00:00
|
|
|
y: newSecondY,
|
2024-11-07 19:19:02 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-11-15 21:34:20 +00:00
|
|
|
const pixelsToPercentage = (pixels: number, totalSize: number): number => {
|
|
|
|
return totalSize > 0 ? (pixels / totalSize) * 100 : 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
const percentageToPixels = (percentage: number, totalSize: number): number => {
|
|
|
|
return (percentage * totalSize) / 100;
|
|
|
|
};
|
|
|
|
|
|
|
|
const getActualPixelDimensions = (): ContainerDimensions => {
|
|
|
|
return {
|
|
|
|
width: Math.round(percentageToPixels(selectorWidth, containerSize.width)),
|
|
|
|
height: Math.round(percentageToPixels(selectorHeight, containerSize.height)),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const writeDimensionsToURL = (): void => {
|
|
|
|
setSearchParams((prev) => ({
|
|
|
|
...prev,
|
|
|
|
x1: ((firstDragable.x / 100) * containerSize.width).toFixed(),
|
|
|
|
y1: ((firstDragable.y / 100) * containerSize.height).toFixed(),
|
|
|
|
x2: ((secondDragable.x / 100) * containerSize.width).toFixed(),
|
|
|
|
y2: ((secondDragable.y / 100) * containerSize.height).toFixed(),
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
const getDisplayValue = (dimension: "width" | "height"): number => {
|
|
|
|
const actualPixels = getActualPixelDimensions();
|
|
|
|
if (unit === "percentage") {
|
|
|
|
return dimension === "width" ? selectorWidth : selectorHeight;
|
|
|
|
}
|
|
|
|
return dimension === "width" ? actualPixels.width : actualPixels.height;
|
|
|
|
};
|
|
|
|
|
2024-11-07 19:19:02 +00:00
|
|
|
const handleDimensionChange = (
|
2024-11-15 21:34:20 +00:00
|
|
|
dimension: "width" | "height",
|
2024-11-07 19:19:02 +00:00
|
|
|
event: React.ChangeEvent<HTMLInputElement>
|
|
|
|
) => {
|
2024-11-15 21:34:20 +00:00
|
|
|
const inputValue = Math.max(0, parseFloat(event.target.value) || 0);
|
|
|
|
let newPercentage: number;
|
2024-11-07 19:19:02 +00:00
|
|
|
|
2024-11-15 21:34:20 +00:00
|
|
|
if (unit === "percentage") {
|
|
|
|
newPercentage = Math.min(inputValue, 100);
|
|
|
|
} else {
|
|
|
|
// Convert pixel input to percentage
|
|
|
|
const totalSize = dimension === "width" ? containerSize.width : containerSize.height;
|
|
|
|
newPercentage = pixelsToPercentage(Math.min(inputValue, totalSize), totalSize);
|
|
|
|
}
|
2024-11-07 19:19:02 +00:00
|
|
|
|
2024-11-15 21:34:20 +00:00
|
|
|
if (dimension === "width") {
|
|
|
|
const newX = Math.min(firstDragable.x, 100 - newPercentage);
|
|
|
|
setFirstDragable((prev) => ({ ...prev, x: newX }));
|
|
|
|
setSecondDragable((prev) => ({ ...prev, x: newX + newPercentage }));
|
2024-11-07 19:19:02 +00:00
|
|
|
} else {
|
2024-11-15 21:34:20 +00:00
|
|
|
const newY = Math.min(firstDragable.y, 100 - newPercentage);
|
|
|
|
setFirstDragable((prev) => ({ ...prev, y: newY }));
|
|
|
|
setSecondDragable((prev) => ({ ...prev, y: newY + newPercentage }));
|
2024-11-07 19:19:02 +00:00
|
|
|
}
|
2024-11-03 21:39:34 +00:00
|
|
|
};
|
2024-11-02 22:38:33 +00:00
|
|
|
|
|
|
|
return (
|
2024-11-16 21:52:33 +00:00
|
|
|
<div className="flex flex-col w-full p-4 h-fit justify-center items-center gap-4">
|
|
|
|
<div
|
|
|
|
ref={containerRef}
|
|
|
|
className="relative w-auto h-fit min-w-40 min-h-40 border-black border"
|
|
|
|
>
|
2024-11-03 21:41:10 +00:00
|
|
|
<DragableBox
|
|
|
|
position={selectorPosition}
|
|
|
|
mode="box"
|
|
|
|
width={selectorWidth}
|
|
|
|
height={selectorHeight}
|
|
|
|
coordSetter={setSelectorPosition}
|
|
|
|
onDrag={handleSelectorDrag}
|
|
|
|
className="border border-black"
|
|
|
|
/>
|
|
|
|
<DragableBox position={firstDragable} coordSetter={setFirstDragable} mode="handle">
|
2024-11-07 19:19:02 +00:00
|
|
|
<div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" />
|
2024-11-03 21:41:10 +00:00
|
|
|
</DragableBox>
|
2024-11-16 21:52:33 +00:00
|
|
|
<img src={image} />
|
2024-11-03 21:41:10 +00:00
|
|
|
<DragableBox position={secondDragable} coordSetter={setSecondDragable} mode="handle">
|
2024-11-07 19:19:02 +00:00
|
|
|
<div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" />
|
2024-11-03 21:41:10 +00:00
|
|
|
</DragableBox>
|
|
|
|
</div>
|
2024-11-15 21:34:20 +00:00
|
|
|
|
|
|
|
<RadioGroup
|
|
|
|
defaultValue="pixels"
|
|
|
|
value={unit}
|
|
|
|
onValueChange={(value) => setUnit(value as UnitType)}
|
|
|
|
className="flex gap-4 mb-2"
|
|
|
|
>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
<RadioGroupItem value="pixels" id="pixels" />
|
|
|
|
<Label htmlFor="pixels">Pixels</Label>
|
|
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
<RadioGroupItem value="percentage" id="percentage" />
|
|
|
|
<Label htmlFor="percentage">Percentage</Label>
|
|
|
|
</div>
|
|
|
|
</RadioGroup>
|
|
|
|
|
2024-11-07 19:19:02 +00:00
|
|
|
<div className="flex gap-4">
|
|
|
|
<div className="flex flex-col gap-2">
|
2024-11-15 21:34:20 +00:00
|
|
|
<label htmlFor="width">Width ({unit === "pixels" ? "px" : "%"})</label>
|
2024-11-07 19:19:02 +00:00
|
|
|
<input
|
|
|
|
id="width"
|
|
|
|
type="number"
|
|
|
|
min={0}
|
2024-11-15 21:34:20 +00:00
|
|
|
max={unit === "percentage" ? 100 : containerSize.width}
|
|
|
|
value={getDisplayValue("width").toFixed(unit === "percentage" ? 1 : 0)}
|
|
|
|
onChange={(e: ChangeEvent<HTMLInputElement>) => handleDimensionChange("width", e)}
|
2024-11-07 19:19:02 +00:00
|
|
|
className="w-24"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
2024-11-15 21:34:20 +00:00
|
|
|
<label htmlFor="height">Height ({unit === "pixels" ? "px" : "%"})</label>
|
2024-11-07 19:19:02 +00:00
|
|
|
<input
|
|
|
|
id="height"
|
|
|
|
type="number"
|
|
|
|
min={0}
|
2024-11-15 21:34:20 +00:00
|
|
|
max={unit === "percentage" ? 100 : containerSize.height}
|
|
|
|
value={getDisplayValue("height").toFixed(unit === "percentage" ? 1 : 0)}
|
|
|
|
onChange={(e) => handleDimensionChange("height", e)}
|
2024-11-07 19:19:02 +00:00
|
|
|
className="w-24"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-11-02 22:38:33 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|