113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import ToolShell from "@/components/toolkit/ToolShell";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
function num(v: string) {
|
|
const n = parseFloat(v);
|
|
return Number.isFinite(n) ? n : 0;
|
|
}
|
|
|
|
export default function Page() {
|
|
const [dpi, setDpi] = useState("300");
|
|
const [lpi, setLpi] = useState("300");
|
|
const [dpcm, setDpcm] = useState("118.11");
|
|
|
|
const [active, setActive] = useState<"dpi" | "lpi" | "dpcm">("dpi");
|
|
|
|
// keep all three in sync based on the most recently edited field
|
|
useEffect(() => {
|
|
const D = num(dpi), L = num(lpi), C = num(dpcm);
|
|
if (active === "dpi") {
|
|
const d = Math.max(1e-9, D);
|
|
setDpcm((d / 2.54).toFixed(5));
|
|
setLpi(D.toFixed(2)); // LPI≈DPI for raster row spacing (workflow convention)
|
|
} else if (active === "lpi") {
|
|
const l = Math.max(1e-9, L);
|
|
setDpi(L.toFixed(2));
|
|
setDpcm((L / 2.54).toFixed(5));
|
|
} else {
|
|
const c = Math.max(1e-9, C);
|
|
setDpi((c * 2.54).toFixed(2));
|
|
setLpi((c * 2.54).toFixed(2));
|
|
}
|
|
}, [dpi, lpi, dpcm, active]);
|
|
|
|
const gapFromDpiMm = 25.4 / Math.max(1e-9, num(dpi));
|
|
const gapFromLpiMm = 25.4 / Math.max(1e-9, num(lpi));
|
|
|
|
return (
|
|
<ToolShell title="DPI / LPI / DPCM Converter">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Values</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-3 sm:grid-cols-3">
|
|
<label className="text-[11px] sm:text-xs">
|
|
<div className="mb-1 text-muted-foreground">DPI</div>
|
|
<Input
|
|
inputMode="decimal"
|
|
value={dpi}
|
|
onChange={(e) => {
|
|
setActive("dpi");
|
|
setDpi(e.target.value);
|
|
}}
|
|
/>
|
|
</label>
|
|
|
|
<label className="text-[11px] sm:text-xs">
|
|
<div className="mb-1 text-muted-foreground">LPI</div>
|
|
<Input
|
|
inputMode="decimal"
|
|
value={lpi}
|
|
onChange={(e) => {
|
|
setActive("lpi");
|
|
setLpi(e.target.value);
|
|
}}
|
|
/>
|
|
</label>
|
|
|
|
<label className="text-[11px] sm:text-xs">
|
|
<div className="mb-1 text-muted-foreground">DPCM</div>
|
|
<Input
|
|
inputMode="decimal"
|
|
value={dpcm}
|
|
onChange={(e) => {
|
|
setActive("dpcm");
|
|
setDpcm(e.target.value);
|
|
}}
|
|
/>
|
|
</label>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="mt-4">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Derived spacing</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-3 sm:grid-cols-3">
|
|
<div>
|
|
<div className="text-sm text-muted-foreground">Pixel/line gap from DPI</div>
|
|
<div className="text-lg">{gapFromDpiMm.toFixed(4)} mm</div>
|
|
<div className="text-xs text-muted-foreground">{(gapFromDpiMm * 1000).toFixed(1)} µm</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-muted-foreground">Line gap from LPI</div>
|
|
<div className="text-lg">{gapFromLpiMm.toFixed(4)} mm</div>
|
|
<div className="text-xs text-muted-foreground">{(gapFromLpiMm * 1000).toFixed(1)} µm</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-muted-foreground">Pixels/cm from DPCM</div>
|
|
<div className="text-lg">{num(dpcm).toFixed(2)} px/cm</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{(num(dpcm) * 2.54).toFixed(2)} px/in
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</ToolShell>
|
|
);
|
|
}
|
|
|