-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
GrainLab is a single-page application built with Vue 3 + TypeScript, bundled by Vite. This page describes the directory structure, data flow, and key subsystems.
src/
├── main.ts # App entry point — mounts Vue, sets up Pinia + i18n
├── App.vue # Root component — layout, keyboard shortcuts, drag-drop
├── env.d.ts # Vite/TypeScript environment declarations
│
├── components/ # UI components
│ ├── ImageCanvas.vue # Canvas-based image preview with zoom/pan
│ ├── ControlPanel.vue # Left panel: preset list + effect adjustment sliders
│ ├── PresetPanel.vue # Preset browser tab
│ ├── ToneCurveEditor.vue # Interactive 3-point tone curve widget
│ ├── ColorWheel.vue # RGB color picker used in Advanced mode
│ ├── FilmStrip.vue # Bottom thumbnail gallery strip
│ ├── BeforeAfter.vue # Draggable split-view comparison overlay
│ ├── BatchPanel.vue # Batch export dialog
│ └── ExportDialog.vue # Single-image export dialog
│
├── filters/ # Image processing core
│ ├── types.ts # TypeScript interfaces for all filter params + FilmPreset
│ ├── pipeline.ts # Applies all filters in order; handles export via canvas
│ ├── filter.worker.ts # Web Worker entry — processes images off the main thread
│ ├── colorGrade.ts # Exposure, temperature, tint, saturation, contrast, toning
│ ├── grain.ts # Film grain noise generation
│ ├── vignette.ts # Radial edge darkening/lightening
│ ├── lightLeak.ts # Corner gradient light leak overlay
│ ├── fade.ts # Black point lift
│ ├── halation.ts # Bright-area color glow
│ ├── bloom.ts # Soft luminance glow
│ └── toneCurve.ts # 3-point luminance spline
│
├── presets/
│ └── index.ts # 11 FilmPreset definitions + cloneParams helper
│
├── stores/ # Pinia state management
│ ├── editor.ts # Active image, current filter params, UI state
│ └── gallery.ts # Image list, thumbnail generation, IndexedDB sync
│
├── i18n/ # Vue I18n translations
│ ├── index.ts # i18n instance, locale detection, LOCALES list
│ ├── en.ts # English (canonical — all other files mirror this shape)
│ ├── zh-CN.ts
│ ├── zh-TW.ts
│ ├── ja.ts
│ ├── ko.ts
│ ├── fr.ts
│ ├── de.ts
│ ├── es.ts
│ └── pt.ts
│
├── utils/
│ ├── fileApi.ts # File reading helpers (readFileAsBase64, etc.)
│ └── galleryDb.ts # IndexedDB CRUD for the gallery
│
└── styles/
└── main.css # Global CSS reset, design tokens, dark/light theme
User Action
│
▼
App.vue / Component
│ calls store action
▼
stores/gallery.ts ──────────────────► utils/galleryDb.ts (IndexedDB)
│ addFiles(), setActive()
▼
stores/editor.ts ◄────────────────── localStorage (params + presetId)
│ params, originalBase64
▼
components/ImageCanvas.vue
│ watches params, triggers re-render
▼
filters/pipeline.ts (main thread, preview)
│ applyFilters(sourceImageData, params)
▼
Canvas 2D Context → displayed on screen
For export:
ExportDialog / BatchPanel
│ calls exportImage()
▼
filters/pipeline.ts :: exportImage()
│ postMessage to Web Worker
▼
filters/filter.worker.ts
│ OffscreenCanvas + applyFilters()
▼
Uint8Array (PNG/JPEG bytes) → Blob → download / ZIP
Manages the currently active image and its filter parameters.
| State | Type | Description |
|---|---|---|
originalBase64 |
string |
Base64-encoded source image (never modified) |
fileName |
string |
Original file name |
imageLoaded |
boolean |
Whether an image is ready to display |
params |
FilterParams |
Reactive filter parameters — persisted to localStorage |
currentPresetId |
string |
Currently applied preset ID — persisted to localStorage |
showBeforeAfter |
boolean |
Split-view comparison mode toggle |
isExporting |
boolean |
Export in-progress flag |
processing |
boolean |
Filter rendering in-progress flag |
Key actions: loadImage(), loadImageWithParams(), applyPreset(), resetParams().
Params are auto-saved to localStorage via a deep watch on params.
Manages the list of all loaded images and drives IndexedDB persistence.
| State | Type | Description |
|---|---|---|
items |
GalleryItem[] |
All gallery entries (file, thumbUrl, params, presetId) |
activeIndex |
number |
Index of the currently displayed image |
Key actions: addFiles(), removeItem(), setActive(), loadFromDB().
Thumbnails are generated at 72×52 px with a center-crop algorithm using an off-screen <canvas>. The active index is also persisted to localStorage (grainlab_active).
Database name: grainlab_gallery (version 1)
Object store: items (keyPath: id)
Each record (PersistedItem) stores:
| Field | Type | Description |
|---|---|---|
id |
string |
Unique ID (timestamp_random) |
name |
string |
Original file name |
type |
string |
MIME type |
buffer |
ArrayBuffer |
Raw image bytes |
thumbUrl |
string |
Base64 thumbnail (JPEG, 72×52) |
params |
FilterParams |
Per-image effect parameters |
presetId |
string |
Applied preset ID |
order |
number |
Display order in the film strip |
src/filters/filter.worker.ts uses OffscreenCanvas to process images without blocking the main thread. Communication is via postMessage / onmessage. The worker receives a base64 source, FilterParams, format, and quality; it returns a Uint8Array of the encoded image.
Locale is auto-detected from navigator.language on first load and can be overridden. The detection logic (src/i18n/index.ts :: getLocale()) maps browser language codes to one of the 9 supported LocaleCode values. The selected code is persisted to localStorage (lang). en is the fallback locale.
Dark/light theme is toggled by adding/removing a CSS class on <html>. Design tokens (colors, spacing) are CSS custom properties defined in src/styles/main.css.
GrainLab ships as a fully installable PWA powered by vite-plugin-pwa (Workbox under the hood).
Defined inline in vite.config.ts:
| Field | Value |
|---|---|
name |
GrainLab – Film Grain & Color Grading |
short_name |
GrainLab |
display |
standalone (hides browser chrome when installed) |
theme_color / background_color
|
#1a1a1a (dark) |
start_url |
. |
| Icons |
pwa-192x192.png, pwa-512x512.png (also used as maskable) |
registerType: 'autoUpdate' — the service worker updates silently in the background. Users get the latest version on the next page load without any manual action.
Workbox pre-caches all build artifacts matching **/*.{js,css,html,ico,png,svg,woff2}, so the app works fully offline after the first visit.
Located in public/:
public/
├── favicon.ico
├── apple-touch-icon.png (180×180, for iOS home screen)
├── pwa-192x192.png (standard Android icon)
└── pwa-512x512.png (large icon + maskable)
To replace the icons, swap out these files and rebuild. The 512×512 image is also used as the maskable icon (ensure your artwork fits within the safe zone — roughly the inner 80% of the canvas).
On desktop Chrome/Edge: click the install icon (⊕) in the address bar.
On Android: tap Add to Home Screen from the browser menu.
On iOS Safari: tap Share → Add to Home Screen.
Using GrainLab
Developer Docs