Skip to content

Concepts ​

This page describes the architecture of Lumina JS. These concepts affect how you trade preview speed against output quality and how you integrate Lumina JS with a bundler.

Non-destructive Editing ​

Lumina JS uses a non-destructive editing model. The editor keeps the original image data unchanged and applies each operation on top of the original, so you can undo operations one at a time or reset the whole edit.

Lightroom and Capture One use the same model.

Preview vs Apply ​

Every adjustment has two 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. The Client API cancels pending preview operations when you call an apply method.

  • 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);

Splitting preview from apply keeps the UI responsive. Previews render fast and leave history unchanged. Apply calls record the edit in history.

Processing Pipeline ​

Lumina JS applies operations in a fixed order, whatever order you call them in, so the same settings produce the same result:

OrderOperationDomain
1Temperature / TintLinear light
2ExposureLinear light
3Highlights / Shadows / MidtonesLinear light
4BrightnessLinear light
5ContrastLinear light
6Color GradingLinear light
—Linear → Perceptual conversion—
7Tonal CurvePerceptual (sRGB)
8SaturationPerceptual (sRGB)

Operations 1–6 run in 32-bit linear HDR space and preserve the full dynamic range of the sensor data. The pipeline applies the perceptual conversion (gamma curve) once, before the tonal curve and saturation steps, which operate in perceptual space.

Preview Sizing ​

The editor scales preview images down for performance. The previewSize config option (default: 1024) sets the maximum dimension in pixels. previewQuality (default: 100) sets the JPEG compression quality of previews.

exportImage() 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. undo() and redo() step through operations in the order you applied them, regardless of type. Applying a new operation clears the redo entries (standard branching behavior).

typescript
await editor.undo();  // undoes the most recent operation
await editor.redo();  // re-applies the most recently undone operation

Core 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 APICore API
ThreadingWeb Worker (off main thread)Main thread or your own worker
Async modelAll methods return PromisesloadImage is async; operations are sync
Preview cancellationAutomatic (stale previews cancelled)Manual
Undo/redoLinear historyPer-operation stacks
Best forBrowser UIsBatch 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 runs on a WebAssembly module (lumina-js.wasm). The API you use determines how Lumina JS locates and loads the .wasm file.

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 from its own script location without extra configuration.

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.

import.meta.url resolves correctly when the runtime loads the module files unbundled (e.g. in a Web Worker or Node.js). If your bundler (Vite, webpack, Rollup) inlines the Lumina modules into a single bundle, import.meta.url points to the bundle file instead of the WASM directory, and the WASM module fails to load.

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;
  },
});

Call configureWasm() once, before Core.Editor.initialize() or Licensing.createDependentKey(). After the WASM module loads, you cannot change the configuration.

Vite ​

Vite does not inline ES module imports by default, so import.meta.url resolves the WASM path and you do not need configureWasm. Serve the WASM files from a public path:

typescript
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    exclude: ['@luminaphoto/lumina-js'],
  },
});

Webpack / Rollup ​

When a bundler inlines modules, import.meta.url stops resolving to the WASM directory. 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 Lumina JS finds the .wasm file without any configuration.

Memory Management ​

Both editors allocate native WASM memory for image data. Call dispose() when you finish with the editor:

typescript
// Client API
await editor.dispose();

// Core API
editor.dispose();

After you call dispose(), you cannot reuse the editor instance. Create a new editor instead.

Proprietary. All rights reserved.