image-converter-client/app/routes/CropSelector.tsx
christian 167a23a000
All checks were successful
Deploy to Cloudflare Pages / deploy (push) Successful in 37s
uploads yay!
2024-11-16 22:52:33 +01:00

229 lines
7.5 KiB
TypeScript

import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import DragableBox from "./DragableBox";
import { Coordinates } from "~/types";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { useSearchParams } from "@remix-run/react";
type ContainerDimensions = {
height: number;
width: number;
};
type FileDimensions = {
x: number;
y: number;
};
type UnitType = "pixels" | "percentage";
interface ComponentProps {
image?: string;
}
export default function CropSelector({ image }: ComponentProps) {
const [firstDragable, setFirstDragable] = useState<Coordinates>({
x: 0,
y: 0,
});
const [secondDragable, setSecondDragable] = useState<Coordinates>({
x: 100,
y: 100,
});
const [selectorPosition, setSelectorPosition] = useState<Coordinates>({
x: 0,
y: 0,
});
const [containerSize, setContainerSize] = useState<ContainerDimensions>({
height: 0,
width: 0,
});
const [pixelDimensions, setPixelDimensions] = useState<ContainerDimensions>({
height: 100,
width: 100,
});
const [unit, setUnit] = useState<UnitType>("pixels");
const [searchParams, setSearchParams] = useSearchParams();
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setSelectorPosition({
x: Math.min(firstDragable.x, secondDragable.x),
y: Math.min(firstDragable.y, secondDragable.y),
});
const setUrlParams = setTimeout(() => {
writeDimensionsToURL();
}, 500);
return () => clearTimeout(setUrlParams);
}, [firstDragable, secondDragable]);
useEffect(() => {
if (!containerRef.current) return;
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
setContainerSize({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
});
resizeObserver.observe(containerRef.current);
return () => resizeObserver.disconnect();
}, [containerRef]);
const selectorWidth = Math.abs(secondDragable.x - firstDragable.x);
const selectorHeight = Math.abs(secondDragable.y - firstDragable.y);
const handleSelectorDrag = (newPosition: Coordinates) => {
const deltaX = newPosition.x - selectorPosition.x;
const deltaY = newPosition.y - selectorPosition.y;
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;
if (newFirstX >= 0 && newSecondX <= 100 && newFirstY >= 0 && newSecondY <= 100) {
setFirstDragable({
x: newFirstX,
y: newFirstY,
});
setSecondDragable({
x: newSecondX,
y: newSecondY,
});
}
};
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;
};
const handleDimensionChange = (
dimension: "width" | "height",
event: React.ChangeEvent<HTMLInputElement>
) => {
const inputValue = Math.max(0, parseFloat(event.target.value) || 0);
let newPercentage: number;
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);
}
if (dimension === "width") {
const newX = Math.min(firstDragable.x, 100 - newPercentage);
setFirstDragable((prev) => ({ ...prev, x: newX }));
setSecondDragable((prev) => ({ ...prev, x: newX + newPercentage }));
} else {
const newY = Math.min(firstDragable.y, 100 - newPercentage);
setFirstDragable((prev) => ({ ...prev, y: newY }));
setSecondDragable((prev) => ({ ...prev, y: newY + newPercentage }));
}
};
return (
<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"
>
<DragableBox
position={selectorPosition}
mode="box"
width={selectorWidth}
height={selectorHeight}
coordSetter={setSelectorPosition}
onDrag={handleSelectorDrag}
className="border border-black"
/>
<DragableBox position={firstDragable} coordSetter={setFirstDragable} mode="handle">
<div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" />
</DragableBox>
<img src={image} />
<DragableBox position={secondDragable} coordSetter={setSecondDragable} mode="handle">
<div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" />
</DragableBox>
</div>
<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>
<div className="flex gap-4">
<div className="flex flex-col gap-2">
<label htmlFor="width">Width ({unit === "pixels" ? "px" : "%"})</label>
<input
id="width"
type="number"
min={0}
max={unit === "percentage" ? 100 : containerSize.width}
value={getDisplayValue("width").toFixed(unit === "percentage" ? 1 : 0)}
onChange={(e: ChangeEvent<HTMLInputElement>) => handleDimensionChange("width", e)}
className="w-24"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="height">Height ({unit === "pixels" ? "px" : "%"})</label>
<input
id="height"
type="number"
min={0}
max={unit === "percentage" ? 100 : containerSize.height}
value={getDisplayValue("height").toFixed(unit === "percentage" ? 1 : 0)}
onChange={(e) => handleDimensionChange("height", e)}
className="w-24"
/>
</div>
</div>
</div>
);
}