# wimp - Architecture Three layers, deliberately separated. React (with the compiler) owns the UI; the heavy pixel work and encoding live in a Web Worker. ## Layer map ``` Layer 1 Interactive UI (main thread, React 19 + compiler) - react-easy-crop: crop / zoom / rotation / flip, live CSS-filter color - toolbar, panels, template picker popover, export panel | | ExportRequest (source bytes transferred, not copied) v Layer 2 Pixel processing (Web Worker, src/workers/export.worker.ts) - decode via createImageBitmap (EXIF-aware) - rotate + flip into the rotation bounding box (OffscreenCanvas) - crop + resample to target size: libvips lanczos3 (if downscaling and available) else canvas 'high' - bake color adjustments; flatten JPEG onto white; apply size cap | v Layer 3 Encoding (same worker) - jSquash MozJPEG / WebP / OxiPNG / AVIF (lazy per format) - fallback: OffscreenCanvas.convertToBlob | | ExportResult { blob, width, height, bytes, engine, encoder } v Main thread: download + result preview ``` ## Data flow 1. A file enters via drag/drop, click, or clipboard paste (src/App.tsx, src/components/Dropzone.tsx). lib/image.ts loads it into an object URL and reads its natural size. 2. react-easy-crop (src/components/EditorCanvas.tsx) displays it. The editor state (crop, zoom, rotation, flip, aspect, output, adjustments, format, quality, maxEdge, template id, croppedAreaPixels) lives in a reducer (src/hooks/useEditor.ts). 3. On export, App reads file.arrayBuffer() and posts an ExportRequest to the worker through lib/exportClient.ts. The ArrayBuffer is transferred (zero copy); a fresh buffer is read for each export so re-export works. 4. exportClient correlates requests and responses by an incrementing id and resolves a promise with the ExportResult. The worker is a lazily created singleton; if it crashes, pending promises reject and the next call spins up a new one. 5. App triggers a download and shows the result (size, dimensions, which engine and encoder ran, plus a preview thumbnail with Save and Copy actions). ## Coordinate model (crop + rotate + flip) This mirrors react-easy-crop's official getCroppedImg recipe so the export matches the preview exactly: - croppedAreaPixels (from onCropComplete) is expressed in the space of the rotated image's bounding box. - The worker rebuilds that exact space: size a canvas to rotateSize(w, h, deg), translate to center, rotate, scale(flip), translate back by -w/2,-h/2, draw. - Flip is applied identically in the preview (Cropper "transform" prop using rotateY/rotateX 180) and in the export canvas, so the same croppedAreaPixels selects the same content in both. ## Shared filter string src/lib/filter.ts builds one CSS/canvas filter string from the Adjustments (brightness, contrast, saturate, grayscale, sepia, hueRotate). The UI uses it as react-easy-crop mediaStyle.filter; the worker uses it as ctx.filter. One source of truth keeps preview and export identical. ## Engines - Pixel engine: libvips (lanczos3) for meaningful downscales when it initializes and the page is cross-origin isolated; otherwise OffscreenCanvas drawImage with imageSmoothingQuality 'high'. See src/lib/vips.ts for the guarded loader. - Encoder: jSquash WASM codecs for real quality control; canvas convertToBlob as fallback. PNG is lossless (quality slider disabled). ## File map ``` index.html entry, inline SVG favicon vite.config.ts react + react-compiler (babel) + tailwind, COOP/COEP headers, wasm optimizeDeps.exclude tsconfig.json single config, DOM + WebWorker libs, skipLibCheck src/ main.tsx React root index.css Tailwind import + @theme tokens + base styles types.ts Template, Adjustments, ExportRequest/Result, msgs templates.ts preset data + helpers (grouped) hooks/useEditor.ts reducer: all editor state + actions lib/ image.ts load, filename, download, byte/MP formatting exportClient.ts main-thread handle to the worker filter.ts shared CSS/canvas filter builder vips.ts guarded lazy wasm-vips loader cn.ts classnames helper workers/export.worker.ts decode -> transform -> encode components/ App is at src/App.tsx layout, file handling, shortcuts, export flow TopBar.tsx header Dropzone.tsx empty-state loader EditorCanvas.tsx Cropper wrapper + framing guide + Fit control TemplatePicker.tsx popover panels.tsx Crop / Transform / Effects / Adjust / Export ui.tsx Button, Section, Segmented Slider.tsx labeled range input icons.tsx inline SVG icons docs/ this documentation ``` ## State shape (reducer) ``` EditorState { crop: { x, y } // react-easy-crop pan position zoom: number // 1..8 rotation: number // degrees, wrapped 0..360 flipH, flipV: boolean aspect: number | null // null = source ratio output: { width, height } | null // null = native cropped size maxEdge: number | null // optional long-edge cap adjustments: Adjustments templateId: string // 'custom' when aspect set manually format: 'jpeg'|'webp'|'png'|'avif' quality: number // 1..100 (ignored for png) croppedAreaPixels: PixelRect | null } ```