Appearance
Concepts
This page explains the architectural ideas behind Lumina JS. Understanding these concepts will help you use the API effectively and make informed choices about performance, quality, and integration.
Non-destructive Editing
Lumina JS uses a non-destructive editing model. The original image data is never modified. Every operation is applied on top of the original, and operations can be individually undone or the entire edit can be reset at any time.
This is the same model used by professional editing tools like Lightroom and Capture One.
Preview vs Apply
Every adjustment has two companion methods:
preview*()— renders the adjustment on a scaled-down copy of the image without recording it in history. Use this while the user is dragging a slider to show real-time feedback. In the Client API, preview operations are automatically cancelled when a new apply operation arrives.apply*()— commits the adjustment to the editing pipeline and records it in the undo/redo history. Use this when the user releases a slider or confirms a value.
typescript
// While the user drags the slider
const { imageData } = await editor.previewExposure(slider.value);
// When the user releases the slider
await editor.applyExposure(slider.value);This separation keeps the UI responsive: previews are fast and disposable, while applies are the source of truth.
Processing Pipeline
All operations are applied in a fixed order, regardless of the order they were called. This ensures consistent, predictable results:
| Order | Operation | Domain |
|---|---|---|
| 1 | Temperature / Tint | Linear light |
| 2 | Exposure | Linear light |
| 3 | Highlights / Shadows / Midtones | Linear light |
| 4 | Brightness | Linear light |
| 5 | Contrast | Linear light |
| 6 | Color Grading | Linear light |
| — | Linear → Perceptual conversion | — |
| 7 | Tonal Curve | Perceptual (sRGB) |
| 8 | Saturation | Perceptual (sRGB) |
Operations 1–6 run in 32-bit linear HDR space, preserving the full dynamic range of the sensor data. The perceptual conversion (gamma curve) is applied once, just before the tonal curve and saturation steps that operate in perceptual space.
Preview Sizing
Preview images are scaled down for performance. The previewSize config option (default: 1024) sets the maximum dimension in pixels. The previewQuality option (default: 100) controls JPEG compression quality for the preview output.
Full-resolution export via exportImage() always processes at the original image dimensions, regardless of preview settings.
Undo / Redo
The two APIs handle undo/redo differently:
Client API — Linear History
The Client API maintains a single chronological undo/redo stack. Operations are undone and redone in the order they were applied, regardless of type. New operations clear the redo tail (standard branching behavior).
typescript
await editor.undo(); // undoes the most recent operation
await editor.redo(); // re-applies the most recently undone operationCore API — Per-operation Stacks
The Core API maintains independent undo/redo stacks for each operation type (depth: 20 per type). You must specify which operation to undo or redo.
typescript
import { Shared } from '@luminaphoto/lumina-js';
editor.undoOperation(Shared.NodeType.Exposure);
editor.redoOperation(Shared.NodeType.Exposure);Client API vs Core API
Both APIs share the same underlying WASM engine and produce identical results. They differ in threading model and async behavior:
| Client API | Core API | |
|---|---|---|
| Threading | Web Worker (off main thread) | Main thread or your own worker |
| Async model | All methods return Promises | loadImage is async; operations are sync |
| Preview cancellation | Automatic (stale previews cancelled) | Manual |
| Undo/redo | Linear history | Per-operation stacks |
| Best for | Browser UIs | Batch pipelines, custom workers, Node.js |
Migrating from Client to Core
The operation methods have the same names and parameters. The main differences are:
typescript
// Client API (async)
const { imageData } = await editor.previewExposure(1.2);
await editor.applyExposure(1.2);
const { data } = await editor.exportImage('jpeg', 95);
// Core API (sync, except loadImage and initialize)
const { imageData } = editor.previewExposure(1.2);
editor.applyExposure(1.2);
const { data } = editor.exportImage('jpeg', 95);Drop the await for operations and export, handle undo/redo per-operation-type, and call editor.dispose() synchronously.
WASM Loading
Lumina JS is powered by a WebAssembly module (lumina-js.wasm). How the .wasm file is located and loaded depends on which API you use.
Client API — Automatic
The Client API runs WASM inside a dedicated Web Worker. You provide the workerPath when creating the editor, and the Worker resolves the .wasm file automatically from its own script location. No extra configuration is needed.
typescript
import { Client } from '@luminaphoto/lumina-js';
const editor = new Client.Editor({
licenseKey: dependentKeyFromServer,
workerPath: '/lumina-js/client/worker.js',
});The Worker script and .wasm file are both under dist/ in the published package. Copy the entire dist/ contents to a public path and point workerPath at the worker script.
Core API and Licensing — configureWasm
The Core API (Core.Editor) and the Licensing module (Licensing.createDependentKey) load WASM on the calling thread. By default, the module uses import.meta.url to locate the .wasm file relative to the module source.
This works when the module files are loaded unbundled (e.g. in a Web Worker or Node.js). However, if your bundler (Vite, webpack, Rollup) inlines the Lumina modules into a single bundle, import.meta.url will point to the bundle file instead of the WASM directory, and loading will fail.
In that case, call Core.configureWasm() before any WASM-dependent call:
typescript
import { Core } from '@luminaphoto/lumina-js';
Core.configureWasm({
locateFile: (path, prefix) => {
if (path.endsWith('.wasm') || path.endsWith('.data')) {
return `/lumina-js/core/wasm/${path}`;
}
return prefix + path;
},
});This must be called once, before Core.Editor.initialize() or Licensing.createDependentKey(). It cannot be changed after the WASM module has loaded.
Vite
Vite does not inline ES module imports by default, so import.meta.url works correctly and configureWasm is not needed. Ensure the WASM files are served from a public path:
typescript
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: {
exclude: ['@luminaphoto/lumina-js'],
},
});Webpack / Rollup
Bundlers that inline modules will break import.meta.url resolution. Use configureWasm to provide an explicit path, as shown above. The path should point to the directory containing lumina-js.wasm.
Node.js
In Node.js, import.meta.url resolves to a file:// URL pointing to the module source, so the .wasm file is found automatically. No configuration is needed.
Memory Management
Both editors hold native WASM memory for image data. Always call dispose() when the editor is no longer needed:
typescript
// Client API
await editor.dispose();
// Core API
editor.dispose();After disposal, the editor instance cannot be reused. Create a new one if needed.