Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 79 additions & 16 deletions src/app/(tools)/svg-to-png/svg-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { useLocalStorage } from "@/hooks/use-local-storage";

import { UploadBox } from "@/components/shared/upload-box";
import { SVGScaleSelector } from "@/components/svg-scale-selector";
import { OptionSelector } from "@/components/option-selector";

export type Scale = "custom" | number;

type ResolutionMode = "scale" | "dimensions";

function scaleSvg(svgContent: string, scale: number) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgContent, "image/svg+xml");
Expand Down Expand Up @@ -141,15 +144,44 @@ function SVGToolCore(props: { fileUploaderProps: FileUploaderResult }) {
const { rawContent, imageMetadata, handleFileUploadEvent, cancel } =
props.fileUploaderProps;

const [resolutionMode, setResolutionMode] = useLocalStorage<ResolutionMode>(
"svgTool_resolutionMode",
"scale"
);
const [scale, setScale] = useLocalStorage<Scale>("svgTool_scale", 1);
const [customScale, setCustomScale] = useLocalStorage<number>(
"svgTool_customScale",
1,
1
);
const [customWidth, setCustomWidth] = useLocalStorage<number>(
"svgTool_customWidth",
imageMetadata?.width ?? 0
);
const [customHeight, setCustomHeight] = useLocalStorage<number>(
"svgTool_customHeight",
imageMetadata?.height ?? 0
);

// Get the actual numeric scale value
const effectiveScale = scale === "custom" ? customScale : scale;

// Calculate dimensions based on mode
const finalDimensions = useMemo(() => {
if (!imageMetadata) return { width: 0, height: 0 };

if (resolutionMode === "scale") {
return {
width: imageMetadata.width * effectiveScale,
height: imageMetadata.height * effectiveScale,
};
} else {
return {
width: customWidth,
height: customHeight,
};
}
}, [imageMetadata, resolutionMode, effectiveScale, customWidth, customHeight]);

if (!imageMetadata)
return (
<UploadBox
Expand All @@ -165,9 +197,7 @@ function SVGToolCore(props: { fileUploaderProps: FileUploaderResult }) {
{/* Preview Section */}
<div className="flex w-full flex-col items-center gap-4 rounded-xl p-6">
<SVGRenderer svgContent={rawContent} />
<p className="text-lg font-medium text-white/80">
{imageMetadata.name}
</p>
<p className="text-lg font-medium text-white/80">{imageMetadata.name}</p>
</div>

{/* Size Information */}
Expand All @@ -180,24 +210,57 @@ function SVGToolCore(props: { fileUploaderProps: FileUploaderResult }) {
</div>

<div className="flex flex-col items-center rounded-lg bg-white/5 p-3">
<span className="text-sm text-white/60">Scaled</span>
<span className="text-sm text-white/60">Output</span>
<span className="font-medium text-white">
{imageMetadata.width * effectiveScale} ×{" "}
{imageMetadata.height * effectiveScale}
{Math.round(finalDimensions.width)} × {Math.round(finalDimensions.height)}
</span>
</div>
</div>

{/* Scale Controls */}
<SVGScaleSelector
title="Scale Factor"
options={[1, 2, 4, 8, 16, 32, 64]}
selected={scale}
onChange={setScale}
customValue={customScale}
onCustomValueChange={setCustomScale}
{/* Resolution Mode Selector */}
<OptionSelector
title="Resolution Mode"
options={["scale", "dimensions"]}
selected={resolutionMode}
onChange={setResolutionMode}
formatOption={(option: ResolutionMode) =>
option === "scale" ? "Scale Factor" : "Custom Dimensions"
}
/>

{/* Scale Controls */}
{resolutionMode === "scale" ? (
<SVGScaleSelector
title="Scale Factor"
options={[1, 2, 4, 8, 16, 32, 64]}
selected={scale}
onChange={setScale}
customValue={customScale}
onCustomValueChange={setCustomScale}
/>
) : (
<div className="flex flex-col items-center gap-4">
<span className="text-sm text-white/60">Dimensions (px)</span>
<div className="flex gap-4">
<input
type="number"
value={customWidth}
onChange={(e) => setCustomWidth(Math.max(1, parseInt(e.target.value) || 0))}
className="w-24 rounded-lg bg-white/5 px-3 py-1.5 text-sm text-white"
placeholder="Width"
/>
<span className="text-white/60">×</span>
<input
type="number"
value={customHeight}
onChange={(e) => setCustomHeight(Math.max(1, parseInt(e.target.value) || 0))}
className="w-24 rounded-lg bg-white/5 px-3 py-1.5 text-sm text-white"
placeholder="Height"
/>
</div>
</div>
)}

{/* Action Buttons */}
<div className="flex gap-3">
<button
Expand All @@ -208,7 +271,7 @@ function SVGToolCore(props: { fileUploaderProps: FileUploaderResult }) {
</button>
<SaveAsPngButton
svgContent={rawContent}
scale={effectiveScale}
scale={resolutionMode === "scale" ? effectiveScale : Math.max(customWidth / imageMetadata.width, customHeight / imageMetadata.height)}
imageMetadata={imageMetadata}
/>
</div>
Expand Down
66 changes: 66 additions & 0 deletions src/components/option-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect, useRef } from "react";

interface OptionSelectorProps<T extends string | number> {
title: string;
options: T[];
selected: T;
onChange: (option: T) => void;
formatOption?: (option: T) => string;
}

export function OptionSelector<T extends string | number>({
title,
options,
selected,
onChange,
formatOption = (option) => `${option}`,
}: OptionSelectorProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const selectedRef = useRef<HTMLButtonElement>(null);
const highlightRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (selectedRef.current && highlightRef.current && containerRef.current) {
const container = containerRef.current;
const selected = selectedRef.current;
const highlight = highlightRef.current;

const containerRect = container.getBoundingClientRect();
const selectedRect = selected.getBoundingClientRect();

highlight.style.left = `${selectedRect.left - containerRect.left}px`;
highlight.style.width = `${selectedRect.width}px`;
}
}, [selected]);

return (
<div className="flex flex-col items-center gap-2">
<span className="text-sm text-white/60">{title}</span>
<div className="flex flex-col items-center gap-2">
<div
ref={containerRef}
className="relative inline-flex rounded-lg bg-white/5 p-1"
>
<div
ref={highlightRef}
className="absolute top-1 h-[calc(100%-8px)] rounded-md bg-blue-600 transition-all duration-200"
/>
{options.map((option) => (
<button
key={String(option)}
ref={option === selected ? selectedRef : null}
onClick={() => onChange(option)}
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"
}`}
>
{formatOption(option)}
</button>
))}
</div>
</div>
</div>
);
}