@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/geometryUsage
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
| Export | Description |
|---|---|
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, Quad | Value 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.
applyMat3functionapplyMat3(m: [number, number, number, number, number, number, number, number, number], p: Point): PointApply 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}.
bilinearWeightfunctionbilinearWeight(x: number): numberBilinear (linear / "tent") reconstruction weight. Support `[-1, 1]`: `bilinearWeight(0) = 1`, `bilinearWeight(±1) = 0`, linear in between.
decomposefunctiondecompose(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.
determinantMat3functiondeterminantMat3(m: [number, number, number, number, number, number, number, number, number]): numberDeterminant of a row-major 3x3 matrix (cofactor expansion along the first row).
fitHomographyfunctionfitHomography(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", …)`.
invertMat3functioninvertMat3(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.
isFinitePointfunctionisFinitePoint(p: Point): booleanTrue when both coordinates are finite real numbers (no NaN / ±Infinity).
kernelWeightsfunctionkernelWeights(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).
lanczosWeightfunctionlanczosWeight(x: number, a?: number): numberLanczos 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.
mitchellWeightfunctionmitchellWeight(x: number, b?: number, c?: number): numberMitchell–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`.
multiplyMat3functionmultiplyMat3(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`).
recomposefunctionrecompose(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).
solveHomographyfunctionsolveHomography(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.
transformFromMatrixfunctiontransformFromMatrix(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", …)`.
Pointinterfaceinterface PointA 2D point in the fixed (top-left origin, Y-down) coordinate space.
Quadtypetype QuadA source/destination quad of exactly four corner points for a homography solve.
AFFINE_BOTTOM_ROW_EPSILONconstAFFINE_BOTTOM_ROW_EPSILON: 1e-9Tolerance for treating a bottom row as the affine `[0, 0, 1]`.
DEGENERATE_DET_EPSILONconstDEGENERATE_DET_EPSILON: 1e-12Determinant 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_AconstLANCZOS_DEFAULT_A: 3Default Lanczos lobe count (`a = 3` — the common high-quality export choice).
MITCHELL_BconstMITCHELL_B: numberMitchell–Netravali B and C parameters used by the engine's bicubic (D8 fixes B = C = 1/3).
MITCHELL_CconstMITCHELL_C: number@faceless-photolib/color
Color science for faceless-photolib — transfer functions, RGB/XYZ primaries, Bradford chromatic adaptation, and OCIO-style hub-and-spoke conversions through an ACEScg (AP1) scene-linear connection space.
@faceless-photolib/blend
All 27 W3C Compositing & Blending Level 1 blend modes (the Photoshop set) over premultiplied Porter-Duff alpha, as a CPU golden reference for faceless-photolib.