Skip to content

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:

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

Proprietary. All rights reserved.