@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/interactionUsage
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
| Export | Description |
|---|---|
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>. |
ControllerState | Lifecycle discriminated union: idle / active / committed / cancelled. |
NormalizedEvent | Surface-agnostic gesture union (begin, drag, setAngle, toggleAspectLock, setAspectPreset, commit, cancel). |
ControllerContext | Active-phase context (source dims, transform, crop rect, aspect lock, pivot, flips); never persisted. |
ControllerCommit | The committed output: { transform, cropRect }. |
ControllerKind | crop / straighten / freeTransform / perspectiveCrop / resize. |
TransformMode | Free-transform sub-mode: move / scale / rotate / skew / distort / perspective. |
CropRect, AspectLock, OverlayGeometry | Crop 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.
beginControllerfunctionbeginController(state: ControllerState, mode: TransformMode, context: ControllerContext): ControllerStateBegin 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.
createControllerfunctioncreateController(kind: ControllerKind, _context: ControllerContext): ControllerStateCreate 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).
dispatchfunctiondispatch(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.
reducefunctionreduce(state: ControllerState, event: NormalizedEvent): ControllerStateThe 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.
AspectLockinterfaceinterface AspectLockAspect-ratio lock — toggled state, not a held modifier.
ControllerCommitinterfaceinterface ControllerCommitThe committed output a controller emits on `commit`.
ControllerContextinterfaceinterface ControllerContextShared context carried while a controller is active; never persisted.
CropRectinterfaceinterface CropRectA crop rectangle in SOURCE pixel coordinates.
OverlayGeometryinterfaceinterface OverlayGeometryOverlay geometry the UI draws (handles, rule-of-thirds grid, crop bounds). Data only — no drawing.
ControllerKindtypetype ControllerKindWhich controller is running.
ControllerStatetypetype ControllerStateController state as a lifecycle discriminated union.
NormalizedEventtypetype NormalizedEventNormalized, surface-agnostic gesture events (UI adapters convert pointer/touch → these).
TransformModetypetype TransformModeFree-transform sub-mode (active phase only).
@faceless-photolib/color-transform
ACES CLF reader and a constrained DCTL subset compiled to the faceless-photolib render IR — the DaVinci-Resolve-style color-transform layer.
@faceless-photolib/document-model
Immutable document model and immer-based op-set over the ordered layer stack (insert/remove/reorder, blend/opacity/transform edits, groups, clipping, masks, locks) for faceless-photolib.