import { useCallback, useEffect, useRef, useState } from "react"; import type { RawImage } from "./imageConvert.ts"; import type { SyntheticSample } from "./samples.ts"; import { SAMPLES } from "./samples.ts"; import { DEFAULT_SCRIPT } from "./examples.ts"; import { createFilterClient } from "./filterClient.ts"; import type { FilterClient } from "./filterClient.ts"; import type { UploadRecord } from "./imageStore.ts"; import { addUpload, deleteUpload, getUploadImage, listUploads, } from "./imageStore.ts"; import { ImageView } from "./components/ImageView.tsx"; import { SamplePicker } from "./components/SamplePicker.tsx"; import { ScriptPanel } from "./components/ScriptPanel.tsx"; interface Loaded { image: RawImage; label: string; } export default function App() { // The worker is created lazily and recreated after teardown. This survives // React StrictMode's dev-only mount→unmount→mount, whose cleanup would // otherwise terminate a worker held in a ref and never replace it. const clientRef = useRef(null); const getClient = useCallback((): FilterClient => { if (!clientRef.current) clientRef.current = createFilterClient(); return clientRef.current; }, []); const [script, setScript] = useState(DEFAULT_SCRIPT); const [original, setOriginal] = useState(null); const [uploads, setUploads] = useState([]); const [selectedId, setSelectedId] = useState(null); const [filtered, setFiltered] = useState(null); const [running, setRunning] = useState(false); const [loadingImage, setLoadingImage] = useState(false); const [error, setError] = useState(null); const [logs, setLogs] = useState([]); const [elapsedMs, setElapsedMs] = useState(null); const scriptRef = useRef(script); scriptRef.current = script; const originalRef = useRef(original); originalRef.current = original; const runIdRef = useRef(0); useEffect(() => { getClient(); // ensure a live worker exists for this mount return () => { clientRef.current?.terminate(); clientRef.current = null; // force a fresh worker on the next mount }; }, [getClient]); const run = useCallback(async () => { const loaded = originalRef.current; if (!loaded) return; const myId = ++runIdRef.current; setRunning(true); try { const res = await getClient().run(scriptRef.current, loaded.image); if (runIdRef.current !== myId) return; setFiltered({ width: res.width, height: res.height, rgba: res.rgba }); setLogs(res.logs); setElapsedMs(res.elapsedMs); setError(null); } catch (e) { if (runIdRef.current !== myId) return; const err = e as Error & { logs?: string[] }; setError(err.message); setLogs(err.logs ?? []); setElapsedMs(null); } finally { if (runIdRef.current === myId) setRunning(false); } }, [getClient]); const selectSample = useCallback((sample: SyntheticSample) => { setSelectedId(sample.id); setError(null); setOriginal({ image: sample.generate(), label: sample.name }); }, []); const selectUpload = useCallback(async (rec: UploadRecord) => { setSelectedId(rec.id); setLoadingImage(true); setError(null); try { const image = await getUploadImage(rec); setOriginal({ image, label: rec.name }); } catch (e) { setError((e as Error).message); } finally { setLoadingImage(false); } }, []); const uploadFile = useCallback(async (file: File) => { setLoadingImage(true); setError(null); try { const { record, image } = await addUpload(file); setUploads((prev) => [record, ...prev]); setSelectedId(record.id); setOriginal({ image, label: record.name }); } catch (e) { setError( `Could not load or store the image: ${(e as Error).message}` ); } finally { setLoadingImage(false); } }, []); const removeUpload = useCallback( async (id: string) => { try { await deleteUpload(id); } catch (e) { setError((e as Error).message); return; } setUploads((prev) => prev.filter((u) => u.id !== id)); if (selectedId === id) selectSample(SAMPLES[0]); }, [selectedId, selectSample] ); // Load persisted uploads, then show a default image, on first mount. useEffect(() => { let cancelled = false; listUploads() .then((list) => { if (!cancelled) setUploads(list); }) .catch(() => { /* ignore — uploads list just stays empty */ }); selectSample(SAMPLES[0]); return () => { cancelled = true; }; }, [selectSample]); // No auto-run: when a new image is loaded, clear the previous result so the // Filtered panel never shows a stale output for a different image. The // filter runs only on the Run button (or ⌘/Ctrl+Enter). useEffect(() => { setFiltered(null); setLogs([]); setElapsedMs(null); }, [original]); return (

numbl image filter

Filter an image with a{" "} numbl {" "} script (MATLAB syntax), run entirely in your browser.

1 · Choose an image

2 · Write a filter

3 · Result

{error &&
⚠ {error}
}
Running…
: null } />
{elapsedMs != null && !error && ( Ran in {elapsedMs.toFixed(0)} ms )} {logs.length > 0 && (
{logs.join("")}
)}
); }