faceless-photolib

@faceless-photolib/geometry

Pure 2D affine + homography math for layer transforms: row-major mat3 algebra, W3C unmatrix decompose/recompose into TRS, 4-point DLT homography, and resampling kernel weights (Y-down convention).

Pure 2D geometry for faceless-photolib — a headless, color-managed, GPU-accelerated image-editing engine.

No DOM / Canvas / GPU imports: just Mat3 algebra, W3C "unmatrix" decompose/recompose into translate–rotate–scale–skew components, a 4-point DLT homography solve (plus over-determined fit), and the 1-D reconstruction-kernel weights backends lower into separable resampling filters. The coordinate convention is fixed: top-left origin, Y-down, with Mat3 stored as a row-major 9-tuple and points transformed as p' = M · [x, y, 1].

Install

pnpm add @faceless-photolib/geometry

Usage

import {
  recompose,
  decompose,
  multiplyMat3,
  invertMat3,
  applyMat3,
  solveHomography,
  kernelWeights,
  mitchellWeight,
} from "@faceless-photolib/geometry";

// Build the canonical matrix from editable TRS components, then apply it.
const matrix = recompose({
  translate: { x: 100, y: 40 },
  rotate: Math.PI / 6,
  scale: { x: 2, y: 2 },
  skew: 0,
  perspective: { x: 0, y: 0 },
});
const moved = applyMat3(matrix, { x: 10, y: 0 });

// Decompose / invert return a Result — degenerate inputs are rejected, never silently approximated.
const decomposed = decompose(matrix); // { kind: "ok", value: { translate, rotate, scale, skew, perspective } }
const inverse = invertMat3(matrix); //   { kind: "rejected", … } when det ≈ 0

// Compose transforms (a applied after b) and solve a perspective warp from corner correspondences.
const composed = multiplyMat3(matrix, inverse.kind === "ok" ? inverse.value : matrix);
const homography = solveHomography(
  [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }],
  [{ x: 0, y: 0 }, { x: 2, y: 0 }, { x: 2, y: 3 }, { x: 0, y: 3 }],
);

// Normalized separable-filter weights for a sub-pixel fraction using a Mitchell bicubic kernel.
const weights = kernelWeights(0.25, 2, mitchellWeight);

API

ExportDescription
multiplyMat3(a, b)Multiply two row-major 3x3 matrices (a applied after b).
determinantMat3(m)Determinant of a row-major 3x3 matrix.
invertMat3(m)Invert via the adjugate; rejected("degenerate", …) when |det| ≤ DEGENERATE_DET_EPSILON.
applyMat3(m, p)Transform a point with the homogeneous perspective divide.
isFinitePoint(p)True when both coordinates are finite (no NaN / ±Infinity).
recompose(d)Build the canonical Mat3 from decomposed TRS + skew + perspective components.
decompose(m)Unmatrix a Mat3 into editable components; the exact inverse of recompose.
transformFromMatrix(m)Build a Transform, classified as affine or perspective.
solveHomography(src, dst)4-point DLT perspective transform between two quads.
fitHomography(src, dst)Over-determined homography fit from n ≥ 4 correspondences.
bilinearWeight(x)Bilinear ("tent") reconstruction weight, support [-1, 1].
mitchellWeight(x, b?, c?)Mitchell–Netravali bicubic weight (default B = C = 1/3), support [-2, 2].
lanczosWeight(x, a?)Lanczos windowed-sinc weight with a lobes (default a = 3).
kernelWeights(fraction, radius, kernel)Normalized separable-pass weights (sum to 1) for a sub-pixel fraction.
Point, QuadValue types: a 2D point and a four-corner quad.

Also exported: the tolerance / parameter constants DEGENERATE_DET_EPSILON, AFFINE_BOTTOM_ROW_EPSILON, MITCHELL_B, MITCHELL_C, and LANCZOS_DEFAULT_A.

Fallible operations (invertMat3, decompose, transformFromMatrix, solveHomography, fitHomography) return a Result<T> from @faceless-photolib/schemas — degenerate / non-invertible inputs are rejected(...) rather than silently flattened to identity.

License

MIT

API reference

21 public exports · 20 documented · generated from source.

applyMat3function
applyMat3(m: [number, number, number, number, number, number, number, number, number], p: Point): Point

Apply a transform matrix to a point with the homogeneous perspective divide. For an affine matrix `w` is `1`; for a perspective matrix the divide projects the point. A point that maps to the plane at infinity (`w ≈ 0`) cannot be represented and produces non-finite coordinates rather than a silent clamp — callers screen for that with {@link isFinitePoint}.

bilinearWeightfunction
bilinearWeight(x: number): number

Bilinear (linear / "tent") reconstruction weight. Support `[-1, 1]`: `bilinearWeight(0) = 1`, `bilinearWeight(±1) = 0`, linear in between.

decomposefunction
decompose(m: [number, number, number, number, number, number, number, number, number]): Result<{ translate: { x: number; y: number; }; rotate: number; scale: { x: number; y: number; }; skew: number; perspective: { ...; }; }>

Decompose a `Mat3` into editable components, the exact inverse of {@link recompose}. A matrix whose affine 2x2 core collapses (a zero-area scale, so the source degenerates to a line or point) is non-invertible and `rejected("degenerate", …)` — never silently flattened to identity.

determinantMat3function
determinantMat3(m: [number, number, number, number, number, number, number, number, number]): number

Determinant of a row-major 3x3 matrix (cofactor expansion along the first row).

fitHomographyfunction
fitHomography(src: readonly Point[], dst: readonly Point[]): Result<[number, number, number, number, number, number, number, number, number]>

Over-determined homography fit from `n ≥ 4` correspondences (Upright-style alignment). Builds the `2n × 8` DLT system and solves the `8 × 8` normal equations `AᵀA h = Aᵀb` by Gaussian elimination, then verifies the fit actually reproduces every correspondence within {@link FIT_RESIDUAL_EPSILON} for exactly-determined inputs. Fewer than four points, mismatched lengths, or a rank-deficient configuration are `rejected("degenerate", …)`.

invertMat3function
invertMat3(m: [number, number, number, number, number, number, number, number, number]): Result<[number, number, number, number, number, number, number, number, number]>

Invert a 3x3 matrix via the adjugate / determinant. A determinant whose magnitude is at or below {@link DEGENERATE_DET_EPSILON} is degenerate and `rejected("degenerate", …)` — never silently approximated to identity.

isFinitePointfunction
isFinitePoint(p: Point): boolean

True when both coordinates are finite real numbers (no NaN / ±Infinity).

kernelWeightsfunction
kernelWeights(fraction: number, radius: number, kernel: (x: number) => number): readonly number[]

Build the normalized weights for a separable 1-D pass at a sub-pixel `fraction ∈ [0, 1)` against the integer sample offsets in `[-radius+1, radius]`, using the supplied even reconstruction kernel. Weights are renormalized to sum to exactly 1 so a flat source is preserved (no energy gain/loss / brightness shift).

lanczosWeightfunction
lanczosWeight(x: number, a?: number): number

Lanczos windowed-sinc reconstruction weight with `a` lobes (support `(-a, a)`): `lanczosWeight(0) = 1`, and `lanczosWeight(n) = 0` for every non-zero integer `n` within the support. `a` MUST be a positive integer.

mitchellWeightfunction
mitchellWeight(x: number, b?: number, c?: number): number

Mitchell–Netravali bicubic reconstruction weight. Support `[-2, 2]`. With the engine's fixed `B = C = 1/3` (D8): `mitchellWeight(0) = 8/9`, `mitchellWeight(±1) = 1/18`, `mitchellWeight(±2) = 0`.

multiplyMat3function
multiplyMat3(a: [number, number, number, number, number, number, number, number, number], b: [number, number, number, number, number, number, number, number, number]): [number, ... 7 more ..., number]

Multiply two row-major 3x3 matrices. Result = `a · b` (`a` applied after `b`).

recomposefunction
recompose(d: { translate: { x: number; y: number; }; rotate: number; scale: { x: number; y: number; }; skew: number; perspective: { x: number; y: number; }; }): [number, number, number, number, number, number, number, number, number]

Build the canonical `Mat3` from decomposed components (the matrix builder).

solveHomographyfunction
solveHomography(src: Quad, dst: Quad): Result<[number, number, number, number, number, number, number, number, number]>

Solve the perspective homography mapping the four `src` corners onto the four `dst` corners (4-point DLT). Collinear or coincident quads have no valid perspective transform and are `rejected("degenerate", …)` — never silently approximated to identity.

transformFromMatrixfunction
transformFromMatrix(m: [number, number, number, number, number, number, number, number, number]): Result<{ kind: "affine"; matrix: [number, number, number, number, number, number, number, number, number]; decomposed: { ...; }; } | { ...; }>

Build a full `Transform` (matrix + decomposed components) from a raw matrix, classifying it as `affine` (bottom row ≈ `[0, 0, 1]`) or `perspective`. A non-invertible matrix is `rejected("degenerate", …)`.

Pointinterface
interface Point

A 2D point in the fixed (top-left origin, Y-down) coordinate space.

Quadtype
type Quad

A source/destination quad of exactly four corner points for a homography solve.

AFFINE_BOTTOM_ROW_EPSILONconst
AFFINE_BOTTOM_ROW_EPSILON: 1e-9

Tolerance for treating a bottom row as the affine `[0, 0, 1]`.

DEGENERATE_DET_EPSILONconst
DEGENERATE_DET_EPSILON: 1e-12

Determinant threshold below which a matrix is treated as non-invertible / degenerate. Chosen well above f64 round-off but small enough that a genuine (if extreme) transform is not falsely rejected.

LANCZOS_DEFAULT_Aconst
LANCZOS_DEFAULT_A: 3

Default Lanczos lobe count (`a = 3` — the common high-quality export choice).

MITCHELL_Bconst
MITCHELL_B: number

Mitchell–Netravali B and C parameters used by the engine's bicubic (D8 fixes B = C = 1/3).

MITCHELL_Cconst
MITCHELL_C: number

On this page