diff --git a/src/app/(tools)/rounded-border/rounded-tool.tsx b/src/app/(tools)/rounded-border/rounded-tool.tsx index a5dbf34..3061bbb 100644 --- a/src/app/(tools)/rounded-border/rounded-tool.tsx +++ b/src/app/(tools)/rounded-border/rounded-tool.tsx @@ -1,4 +1,5 @@ "use client"; + import { usePlausible } from "next-plausible"; import { useEffect, useMemo, useRef, useState } from "react"; import { useLocalStorage } from "@/hooks/use-local-storage"; @@ -14,21 +15,35 @@ import { FileDropzone } from "@/components/shared/file-dropzone"; type Radius = number; type BackgroundOption = "white" | "black" | "transparent"; +type ShapeOption = "rounded" | "circle"; function useImageConverter(props: { canvas: HTMLCanvasElement | null; imageContent: string; radius: Radius; + shapeOption: ShapeOption; background: BackgroundOption; fileName?: string; imageMetadata: { width: number; height: number; name: string }; }) { - const { width, height } = useMemo(() => { - return { - width: props.imageMetadata.width, - height: props.imageMetadata.height, - }; - }, [props.imageMetadata]); + const { width, height, offsetX, offsetY } = useMemo(() => { + if (props.shapeOption === "circle") { + const size = Math.min( + props.imageMetadata.width, + props.imageMetadata.height, + ); + const offsetX = (props.imageMetadata.width - size) / 2; + const offsetY = (props.imageMetadata.height - size) / 2; + return { width: size, height: size, offsetX, offsetY }; + } else { + return { + width: props.imageMetadata.width, + height: props.imageMetadata.height, + offsetX: 0, + offsetY: 0, + }; + } + }, [props.imageMetadata, props.shapeOption]); const convertToPng = async () => { const ctx = props.canvas?.getContext("2d"); @@ -47,22 +62,48 @@ function useImageConverter(props: { const img = new Image(); img.onload = () => { + props.canvas!.width = width; + props.canvas!.height = height; + ctx.clearRect(0, 0, width, height); ctx.fillStyle = props.background; ctx.fillRect(0, 0, width, height); - ctx.beginPath(); - ctx.moveTo(props.radius, 0); - ctx.lineTo(width - props.radius, 0); - ctx.quadraticCurveTo(width, 0, width, props.radius); - ctx.lineTo(width, height - props.radius); - ctx.quadraticCurveTo(width, height, width - props.radius, height); - ctx.lineTo(props.radius, height); - ctx.quadraticCurveTo(0, height, 0, height - props.radius); - ctx.lineTo(0, props.radius); - ctx.quadraticCurveTo(0, 0, props.radius, 0); - ctx.closePath(); - ctx.clip(); - ctx.drawImage(img, 0, 0, width, height); + + if (props.shapeOption === "circle") { + ctx.save(); + ctx.beginPath(); + ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI); + ctx.closePath(); + ctx.clip(); + + ctx.drawImage( + img, + offsetX, + offsetY, + width, + height, + 0, + 0, + width, + height, + ); + ctx.restore(); + } else { + const radius = props.radius; + ctx.beginPath(); + ctx.moveTo(radius, 0); + ctx.lineTo(width - radius, 0); + ctx.quadraticCurveTo(width, 0, width, radius); + ctx.lineTo(width, height - radius); + ctx.quadraticCurveTo(width, height, width - radius, height); + ctx.lineTo(radius, height); + ctx.quadraticCurveTo(0, height, 0, height - radius); + ctx.lineTo(0, radius); + ctx.quadraticCurveTo(0, 0, radius, 0); + ctx.closePath(); + ctx.clip(); + ctx.drawImage(img, 0, 0, width, height); + } saveImage(); }; @@ -71,19 +112,21 @@ function useImageConverter(props: { return { convertToPng, - canvasProps: { width: width, height: height }, + canvasProps: { width, height }, }; } interface ImageRendererProps { imageContent: string; radius: Radius; + shapeOption: ShapeOption; background: BackgroundOption; } const ImageRenderer = ({ imageContent, radius, + shapeOption, background, }: ImageRendererProps) => { const containerRef = useRef(null); @@ -92,22 +135,36 @@ const ImageRenderer = ({ if (containerRef.current) { const imgElement = containerRef.current.querySelector("img"); if (imgElement) { - imgElement.style.borderRadius = `${radius}px`; + if (shapeOption === "circle") { + imgElement.style.borderRadius = "50%"; + imgElement.style.objectFit = "cover"; + imgElement.style.width = "100%"; + imgElement.style.height = "100%"; + } else { + imgElement.style.borderRadius = `${radius}px`; + imgElement.style.objectFit = "contain"; + imgElement.style.width = "100%"; + imgElement.style.height = "auto"; + } } } - }, [imageContent, radius]); + }, [imageContent, radius, shapeOption]); return ( -
-
+
Preview
); @@ -116,11 +173,13 @@ const ImageRenderer = ({ function SaveAsPngButton({ imageContent, radius, + shapeOption, background, imageMetadata, }: { imageContent: string; radius: Radius; + shapeOption: ShapeOption; background: BackgroundOption; imageMetadata: { width: number; height: number; name: string }; }) { @@ -129,6 +188,7 @@ function SaveAsPngButton({ canvas: canvasRef, imageContent, radius, + shapeOption, background, imageMetadata, }); @@ -160,6 +220,7 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) { "roundedTool_background", "transparent", ); + const [shapeOption, setShapeOption] = useState("rounded"); const handleRadiusChange = (value: number | "custom") => { if (value === "custom") { @@ -183,11 +244,12 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) { } return ( -
-
+
+

@@ -196,20 +258,38 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) {

- Original Size + + {shapeOption === "circle" ? "Cropped Size" : "Original Size"} + - {imageMetadata.width} × {imageMetadata.height} + {shapeOption === "circle" + ? `${Math.min(imageMetadata.width, imageMetadata.height)} × ${Math.min( + imageMetadata.width, + imageMetadata.height, + )}` + : `${imageMetadata.width} × ${imageMetadata.height}`}
- + setShapeOption(option === "Circle" ? "circle" : "rounded") + } + formatOption={(option) => option} /> + {shapeOption === "rounded" && ( + + )} diff --git a/src/components/border-radius-selector.tsx b/src/components/border-radius-selector.tsx index 8ff2ed8..0aac7cc 100644 --- a/src/components/border-radius-selector.tsx +++ b/src/components/border-radius-selector.tsx @@ -7,6 +7,7 @@ interface BorderRadiusSelectorProps { onChange: (value: number | "custom") => void; customValue?: number; onCustomValueChange?: (value: number) => void; + disabled?: boolean; // Added the optional 'disabled' prop } export function BorderRadiusSelector({ @@ -16,6 +17,7 @@ export function BorderRadiusSelector({ onChange, customValue, onCustomValueChange, + disabled, // Destructure the 'disabled' prop }: BorderRadiusSelectorProps) { const containerRef = useRef(null); const selectedRef = useRef(null); @@ -24,11 +26,11 @@ export function BorderRadiusSelector({ useEffect(() => { if (selectedRef.current && highlightRef.current && containerRef.current) { const container = containerRef.current; - const selected = selectedRef.current; + const selectedButton = selectedRef.current; const highlight = highlightRef.current; const containerRect = container.getBoundingClientRect(); - const selectedRect = selected.getBoundingClientRect(); + const selectedRect = selectedButton.getBoundingClientRect(); highlight.style.left = `${selectedRect.left - containerRect.left}px`; highlight.style.width = `${selectedRect.width}px`; @@ -79,17 +81,18 @@ export function BorderRadiusSelector({ onClick={() => onChange(typeof option === "number" ? option : "custom") } + disabled={disabled} className={`relative rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${ option === selected ? "text-white" : "text-white/80 hover:text-white" - }`} + } ${disabled ? "cursor-not-allowed opacity-50" : ""}`} > - {option === "custom" ? "Custom" : option} + {option === "custom" ? "Custom" : `${option}px`} ))}
- {selected === "custom" && ( + {selected === "custom" && !disabled && (