"use client"; import { useState, useEffect } from "react"; import { useSearchParams } from "next/navigation"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"; const ERROR_DIFFUSION = [ { value: "floyd-steinberg", label: "Floyd-Steinberg" }, { value: "atkinson", label: "Atkinson" }, { value: "stucki", label: "Stucki" }, { value: "burkes", label: "Burkes" }, { value: "sierra", label: "Sierra" }, { value: "sierra-two-row", label: "Sierra Two-Row" }, { value: "sierra-lite", label: "Sierra Lite" }, { value: "jarvis-judice-ninke", label: "Jarvis-Judice-Ninke" }, ]; const ORDERED = [ { value: "bayer", label: "Bayer" }, { value: "ordered", label: "Ordered" }, { value: "cluster-dot", label: "Cluster Dot" }, { value: "yliluoma", label: "Yliluoma" }, ]; const THRESHOLD_ALGORITHMS = ["bayer", "ordered", "cluster-dot"]; const ORDER_ALGORITHMS = ["bayer", "ordered", "cluster-dot", "yliluoma"]; interface Design { slug: string; name: string; image_url: string; status: string; } interface DitherResponse { slug: string; algorithm: string; palette_mode: string; num_colors: number; colors_used: string[]; cached: boolean; image_url: string; } export default function HitherditherPage() { const searchParams = useSearchParams(); const [designs, setDesigns] = useState([]); const [slug, setSlug] = useState(searchParams.get("slug") || ""); const [algorithm, setAlgorithm] = useState("floyd-steinberg"); const [palette, setPalette] = useState("auto"); const [numColors, setNumColors] = useState(8); const [threshold, setThreshold] = useState(64); const [order, setOrder] = useState(8); const [metadata, setMetadata] = useState(null); const [ditherUrl, setDitherUrl] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Fetch designs on mount useEffect(() => { fetch(`${API_URL}/designs?status=active`) .then((res) => (res.ok ? res.json() : Promise.reject(res))) .then((data: Design[]) => setDesigns(data)) .catch(() => setError("Failed to load designs")); }, []); const showThreshold = THRESHOLD_ALGORITHMS.includes(algorithm); const showOrder = ORDER_ALGORITHMS.includes(algorithm); const buildDitherParams = () => { const params = new URLSearchParams({ algorithm, palette, num_colors: String(numColors), }); if (showThreshold) params.set("threshold", String(threshold)); if (showOrder) params.set("order", String(order)); return params; }; const handleApply = async () => { if (!slug) return; setIsLoading(true); setError(null); try { const params = buildDitherParams(); params.set("format", "json"); const response = await fetch( `${API_URL}/designs/${slug}/dither?${params}` ); if (!response.ok) { let message = "Dithering failed"; try { const data = await response.json(); message = data.detail || message; } catch { const text = await response.text(); message = text || `Server error (${response.status})`; } throw new Error(message); } const data: DitherResponse = await response.json(); setMetadata(data); // Build image URL (without format=json) const imgParams = buildDitherParams(); setDitherUrl(`${API_URL}/designs/${slug}/dither?${imgParams}`); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { setIsLoading(false); } }; return (

Hitherdither

Apply retro dithering effects to any design in the catalog

{/* Left: Controls */}
{/* Design Picker */}
{/* Algorithm */}
{/* Palette */}
{/* Number of Colors */}
setNumColors(Number(e.target.value))} className="w-full" />
2 32
{/* Threshold (conditional) */} {showThreshold && (
setThreshold(Number(e.target.value))} className="w-full" />
1 256
)} {/* Order (conditional) */} {showOrder && (
setOrder(Number(e.target.value))} className="w-full" />
2 16
)} {error && (
{error}
)} {/* Apply Button */}
{/* Right: Preview */}

Preview

{slug ? (
{/* Side-by-side comparison */}

Original

Original design

Dithered

{ditherUrl ? ( Dithered result ) : (

Click “Apply Dither” to see result

)}
{/* Palette display */} {metadata && (
{metadata.colors_used.map((color) => (
{color}
))}

{metadata.num_colors} colors {metadata.cached ? " · cached" : ""}

)} {/* Download */} {ditherUrl && ( Download PNG )}
) : (

Select a design to get started

)}
{/* Tips */}

How It Works

  • Error diffusion algorithms (Floyd-Steinberg, Atkinson, etc.) spread quantization error to neighboring pixels for natural-looking results
  • Ordered algorithms (Bayer, Cluster Dot) use fixed threshold patterns for a more structured, retro look
  • • Fewer colors produce more dramatic dithering effects — try 2-4 colors for a classic screen-print aesthetic
  • • First-time dithering for a design may take 5-12 seconds; subsequent requests with the same settings are cached
); }