/ concept-collection / numbl-image-filter
Sign in
concept-collection / numbl-image-filter
numbl-image-filter / src / components / SamplePicker.tsx
122 lines · 3.5 KBBlameHistoryRaw
1import { useEffect, useRef } from "react";
2import type { SyntheticSample } from "../samples.ts";
3import type { UploadRecord } from "../imageStore.ts";
5function SyntheticThumb({ sample }: { sample: SyntheticSample }) {
6 const ref = useRef<HTMLCanvasElement>(null);
7 useEffect(() => {
8 const canvas = ref.current;
9 if (!canvas) return;
10 const raw = sample.generate();
11 const tmp = document.createElement("canvas");
12 tmp.width = raw.width;
13 tmp.height = raw.height;
14 tmp
15 .getContext("2d")!
16 .putImageData(
17 new ImageData(new Uint8ClampedArray(raw.rgba), raw.width, raw.height),
18 0,
19 0
20 );
21 const ctx = canvas.getContext("2d")!;
22 ctx.clearRect(0, 0, canvas.width, canvas.height);
23 ctx.drawImage(tmp, 0, 0, canvas.width, canvas.height);
24 }, [sample]);
25 return <canvas ref={ref} width={72} height={72} className="thumb-canvas" />;
28interface Props {
29 samples: SyntheticSample[];
30 uploads: UploadRecord[];
31 selectedId: string | null;
32 onSelectSample: (sample: SyntheticSample) => void;
33 onSelectUpload: (rec: UploadRecord) => void;
34 onUpload: (file: File) => void;
35 onDeleteUpload: (id: string) => void;
36 disabled?: boolean;
39export function SamplePicker({
40 samples,
41 uploads,
42 selectedId,
43 onSelectSample,
44 onSelectUpload,
45 onUpload,
46 onDeleteUpload,
47 disabled,
48}: Props) {
49 const fileRef = useRef<HTMLInputElement>(null);
51 return (
52 <div className="picker">
53 <div className="picker-row">
54 {samples.map((sample) => (
55 <button
56 key={sample.id}
57 className={
58 "sample" + (sample.id === selectedId ? " sample-selected" : "")
59 }
60 onClick={() => onSelectSample(sample)}
61 disabled={disabled}
62 title="Generated"
63 >
64 <SyntheticThumb sample={sample} />
65 <span className="sample-name">{sample.name}</span>
66 </button>
67 ))}
69 {uploads.length > 0 && <div className="picker-divider" aria-hidden />}
71 {uploads.map((rec) => (
72 <div className="sample-wrap" key={rec.id}>
73 <button
74 className={
75 "sample" + (rec.id === selectedId ? " sample-selected" : "")
76 }
77 onClick={() => onSelectUpload(rec)}
78 disabled={disabled}
79 title={`${rec.name} · ${rec.width}×${rec.height}`}
80 >
81 <img className="thumb-canvas" src={rec.thumb} alt={rec.name} />
82 <span className="sample-name">{rec.name}</span>
83 </button>
84 <button
85 className="sample-delete"
86 title="Delete"
87 aria-label={`Delete ${rec.name}`}
88 onClick={(e) => {
89 e.stopPropagation();
90 onDeleteUpload(rec.id);
91 }}
92 disabled={disabled}
93 >
94 ×
95 </button>
96 </div>
97 ))}
99 <button
100 className="sample sample-upload"
101 onClick={() => fileRef.current?.click()}
102 disabled={disabled}
103 title="Upload an image (stored locally in your browser)"
104 >
105 <span className="upload-plus">+</span>
106 <span className="sample-name">Upload</span>
107 </button>
108 <input
109 ref={fileRef}
110 type="file"
111 accept="image/*"
112 style={{ display: "none" }}
113 onChange={(e) => {
114 const f = e.target.files?.[0];
115 if (f) onUpload(f);
116 e.target.value = "";
117 }}
118 />
119 </div>
120 </div>
121 );
moveopenescclose