faceless-photolib

@faceless-photolib/interaction

Headless crop / resize / rotate / straighten / free-transform interaction controllers — pure state-machine reducers that emit committed transforms and crop rects, replicating Photoshop and Lightroom crop+transform controls without any UI.

Headless crop / resize / rotate / straighten / free-transform interaction controllers for faceless-photolib — a headless, color-managed, GPU-accelerated image-editing engine.

These are pure state machines, not UI. Each controller is a deterministic (state, event) -> newState reducer with an idle -> active -> committed | cancelled lifecycle, replicating the Photoshop and Lightroom crop + transform controls. Interaction-temporary state is never persisted; only the committed { transform, cropRect } reaches the document. Transforms map directly onto @faceless-photolib/geometry Mat3 values. No DOM, Canvas, or React — UI adapters convert pointer/touch input into the surface-agnostic NormalizedEvent vocabulary and draw the supplied OverlayGeometry themselves.

Install

pnpm add @faceless-photolib/interaction

Usage

import {
  createController,
  beginController,
  dispatch,
  type ControllerContext,
} from "@faceless-photolib/interaction";

// Document-level context: source dimensions, base transform, crop rect, etc.
declare const context: ControllerContext;

// 1. Create an idle controller, then enter `active` with the document context.
//    (`begin` needs out-of-band context, so it goes through beginController,
//    not the bare-event reducer.)
let state = createController("crop", context);
state = beginController(state, "scale", context);

// 2. Feed normalized, surface-agnostic gesture events through the transport
//    boundary. `dispatch` validates the untrusted event and returns a Result:
//    `invalid-request` for malformed events or undefined transitions, or
//    `rejected("not-implemented", …)` for phased-later gestures.
const dragged = dispatch(state, { type: "drag", delta: { x: 24, y: 0 } });
if (dragged.ok) state = dragged.value;

// 3. Commit. The committed state carries only `{ transform, cropRect }`.
const committed = dispatch(state, { type: "commit" });
if (committed.ok && committed.value.lifecycle === "committed") {
  const { transform, cropRect } = committed.value.commit;
  // …apply to the document.
}

API

ExportDescription
createController(kind, context)Creates a controller in its idle lifecycle state for the given ControllerKind.
beginController(state, mode, context)Enters active(mode) from idle, seeded with the pre-interaction document context.
reduce(state, event)The pure, deterministic reducer; an undefined transition leaves state unchanged.
dispatch(state, event)Transport boundary: validates an untrusted event and applies reduce, returning a Result<ControllerState>.
ControllerStateLifecycle discriminated union: idle / active / committed / cancelled.
NormalizedEventSurface-agnostic gesture union (begin, drag, setAngle, toggleAspectLock, setAspectPreset, commit, cancel).
ControllerContextActive-phase context (source dims, transform, crop rect, aspect lock, pivot, flips); never persisted.
ControllerCommitThe committed output: { transform, cropRect }.
ControllerKindcrop / straighten / freeTransform / perspectiveCrop / resize.
TransformModeFree-transform sub-mode: move / scale / rotate / skew / distort / perspective.
CropRect, AspectLock, OverlayGeometryCrop rectangle (source pixels), aspect-ratio lock, and UI overlay geometry (handles + rule-of-thirds grid).

Controllers are deterministic — they read no clock, randomness, or ambient state. Crop-family controllers (crop / resize / straighten / perspectiveCrop) adjust the crop rect only; freeTransform composes the image transform only. The distort and perspective free-transform sub-modes require four-corner homography state not yet modelled and surface as rejected("not-implemented", …) rather than silently degrading.

License

MIT

API reference

13 public exports · 13 documented · generated from source.

beginControllerfunction
beginController(state: ControllerState, mode: TransformMode, context: ControllerContext): ControllerState

Begin an interaction from `idle`: enter `active(mode)` seeded from the supplied context. The seed context is the pre-interaction snapshot; `cancel` is a no-op that returns to a terminal `cancelled` state carrying no commit, so the caller's retained context is what is restored.

createControllerfunction
createController(kind: ControllerKind, _context: ControllerContext): ControllerState

Create a controller in its `idle` lifecycle state. The seed `_context` is the caller-retained snapshot used to begin (and, by re-creating, to restore on cancel).

dispatchfunction
dispatch(state: ControllerState, event: unknown): Result<ControllerState>

Validate a normalized event at the transport boundary and apply the reducer. Malformed events return `invalid-request`; an invalid lifecycle transition returns `invalid-request` and leaves state unchanged; an unimplemented gesture/mode returns `rejected("not-implemented", …)` with a beacon.

reducefunction
reduce(state: ControllerState, event: NormalizedEvent): ControllerState

The pure reducer. Deterministic — no clock, randomness, or ambient state. An event that is not a defined transition from the current state leaves the state unchanged (the transport boundary maps that to `invalid-request`). Dispatches on the lifecycle variant first (exhaustive), so a new lifecycle variant is a compile error here.

AspectLockinterface
interface AspectLock

Aspect-ratio lock — toggled state, not a held modifier.

ControllerCommitinterface
interface ControllerCommit

The committed output a controller emits on `commit`.

ControllerContextinterface
interface ControllerContext

Shared context carried while a controller is active; never persisted.

CropRectinterface
interface CropRect

A crop rectangle in SOURCE pixel coordinates.

OverlayGeometryinterface
interface OverlayGeometry

Overlay geometry the UI draws (handles, rule-of-thirds grid, crop bounds). Data only — no drawing.

ControllerKindtype
type ControllerKind

Which controller is running.

ControllerStatetype
type ControllerState

Controller state as a lifecycle discriminated union.

NormalizedEventtype
type NormalizedEvent

Normalized, surface-agnostic gesture events (UI adapters convert pointer/touch → these).

TransformModetype
type TransformMode

Free-transform sub-mode (active phase only).

On this page