Skip to content

Architecture

Flying Pizza edited this page Apr 19, 2026 · 3 revisions

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.


Directory Structure

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

Data Flow

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

Pinia Stores

stores/editor.tsuseEditorStore

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.

stores/gallery.tsuseGalleryStore

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).


IndexedDB Schema

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

Web Worker

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.


i18n

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.


Theme

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.


PWA (Progressive Web App)

GrainLab ships as a fully installable PWA powered by vite-plugin-pwa (Workbox under the hood).

Manifest

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)

Service Worker

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.

Icon files

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).

Installing the app

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.

Clone this wiki locally