Fluent API
The fluent API provides an ergonomic, chainable interface for composing transforms and rendering operations. It offers two styles:
- Builder pattern - Method chaining with
PathBuilderandBitmapBuilderclasses - Pipe pattern - Functional composition with curried operators
Quick Start
Builder Style
import { glyph } from "text-shaper";
const rgba = glyph(font, glyphId)
?.scale(2)
.rotateDeg(15)
.rasterizeAuto({ padding: 2 })
.blur(5)
.toRGBA();Pipe Style
import { pipe, $scale, $rotate, $rasterize, $blur, $toRGBA, getGlyphPath } from "text-shaper";
const rgba = pipe(
getGlyphPath(font, glyphId),
$scale(2, 2),
$rotate(Math.PI / 4),
$rasterize({ width: 100, height: 100 }),
$blur(5),
$toRGBA
);Entry Points
glyph()
Create a PathBuilder from a font glyph.
function glyph(font: Font, glyphId: GlyphId): PathBuilder | nullExample:
const builder = glyph(font, glyphId);
if (builder) {
const rgba = builder.scale(2).rasterizeAuto().toRGBA();
}char()
Create a PathBuilder from a character.
function char(font: Font, character: string): PathBuilder | nullExample:
const builder = char(font, "A");
const rgba = builder?.scale(2).rasterizeAuto().toRGBA();glyphVar()
Create a PathBuilder from a variable font glyph with axis coordinates.
function glyphVar(font: Font, glyphId: GlyphId, axisCoords: number[]): PathBuilder | nullExample:
// Create with weight=700, width=100
const builder = glyphVar(font, glyphId, [700, 100]);path()
Wrap an existing GlyphPath in a PathBuilder.
function path(p: GlyphPath): PathBuilderExample:
const existingPath = getGlyphPath(font, glyphId);
if (existingPath) {
const rgba = path(existingPath).scale(2).rasterizeAuto().toRGBA();
}bitmap()
Wrap an existing Bitmap in a BitmapBuilder.
function bitmap(b: Bitmap): BitmapBuilderExample:
const existingBitmap = rasterizePath(path, options);
const rgba = bitmap(existingBitmap).blur(5).toRGBA();combine()
Combine multiple PathBuilder instances into one.
function combine(...paths: PathBuilder[]): PathBuilderExample:
const h = glyph(font, hGlyphId)?.translate(0, 0);
const i = glyph(font, iGlyphId)?.translate(100, 0);
if (h && i) {
const combined = combine(h, i).scale(2).rasterizeAuto().toRGBA();
}PathBuilder
The PathBuilder class provides fluent methods for path transformations and rendering.
Static Factory Methods
These are typically accessed via the entry point functions, but can also be called directly.
PathBuilder.fromGlyph()
Create from a font glyph.
static fromGlyph(font: Font, glyphId: GlyphId): PathBuilder | nullPathBuilder.fromGlyphWithVariation()
Create from a variable font glyph with axis coordinates.
static fromGlyphWithVariation(font: Font, glyphId: GlyphId, axisCoords: number[]): PathBuilder | nullPathBuilder.fromPath()
Create from an existing GlyphPath.
static fromPath(path: GlyphPath): PathBuilderPathBuilder.combine()
Combine multiple PathBuilders into one.
static combine(...builders: PathBuilder[]): PathBuilderLazy Transforms
Transforms are accumulated as a matrix and applied lazily when rendering or when .apply() is called.
scale()
Scale uniformly or non-uniformly.
scale(sx: number, sy?: number): PathBuildertranslate()
Translate by offset.
translate(dx: number, dy: number): PathBuilderrotate()
Rotate by angle in radians.
rotate(angle: number): PathBuilderrotateDeg()
Rotate by angle in degrees.
rotateDeg(angleDeg: number): PathBuildershear()
Apply shear/skew transformation.
shear(shearX: number, shearY: number): PathBuilderitalic()
Apply italic slant (angle in degrees, typically 12-15).
italic(angleDeg: number): PathBuildermatrix()
Apply custom 2D affine matrix.
matrix(m: Matrix2D): PathBuilderperspective()
Apply 3D perspective matrix.
perspective(m: Matrix3x3): PathBuilderperspectiveVanish()
Create perspective with vanishing point.
perspectiveVanish(vanishingPointX: number, vanishingPointY: number, strength: number): PathBuilderresetTransform()
Reset transform to identity.
resetTransform(): PathBuilderEager Path Effects
These effects apply immediately (force transform application first).
apply()
Force application of pending transforms.
apply(): PathBuilderExample:
const path = glyph(font, glyphId)
?.scale(2)
.rotate(Math.PI / 4)
.apply() // Apply transforms now
.embolden(50) // Then embolden the transformed path
.toPath();embolden()
Synthetic bold effect.
embolden(strength: number): PathBuildercondense()
Horizontal condensing (factor < 1) or expansion (factor > 1).
condense(factor: number): PathBuilderoblique()
Apply oblique/slant transformation.
oblique(slant: number): PathBuilderstroke()
Convert to stroked outline.
stroke(options: StrokerOptions): PathBuilder
stroke(width: number, cap?: LineCap, join?: LineJoin): PathBuilderExample:
const stroked = glyph(font, glyphId)
?.stroke({ width: 20, lineCap: "round", lineJoin: "round" })
.scale(2)
.rasterizeAuto()
.toRGBA();Bounds & Metrics
controlBox()
Get bounding box of control points (after transforms).
controlBox(): { xMin: number; yMin: number; xMax: number; yMax: number }tightBounds()
Get exact bounds considering curve extrema (after transforms).
tightBounds(): { xMin: number; yMin: number; xMax: number; yMax: number }getTransformMatrix()
Get the accumulated 2D transform matrix.
getTransformMatrix(): Matrix2DgetTransformMatrix3D()
Get the accumulated 3D transform matrix.
getTransformMatrix3D(): Matrix3x3 | nullRasterization
rasterize()
Rasterize to bitmap with explicit size.
rasterize(options: RasterOptions): BitmapBuilderOptions:
interface RasterOptions {
width?: number;
height?: number;
scale?: number;
offsetX?: number;
offsetY?: number;
padding?: number;
pixelMode?: PixelMode;
fillRule?: FillRule;
flipY?: boolean;
}rasterizeAuto()
Rasterize with auto-computed size from bounds.
rasterizeAuto(options?: AutoRasterOptions): BitmapBuilderOptions:
interface AutoRasterOptions {
padding?: number; // Default: 1
scale?: number; // Default: 1
pixelMode?: PixelMode;
fillRule?: FillRule;
flipY?: boolean;
}Example:
const rgba = glyph(font, glyphId)
?.scale(2)
.rasterizeAuto({ padding: 5 })
.toRGBA();rasterizeWithGradient()
Rasterize with gradient fill.
rasterizeWithGradient(gradient: Gradient, options: RasterOptions): BitmapBuilderExample:
import { glyph, type LinearGradient } from "text-shaper";
const gradient: LinearGradient = {
type: "linear",
x0: 0, y0: 0,
x1: 100, y1: 100,
stops: [
{ offset: 0, color: [255, 0, 0, 255] },
{ offset: 1, color: [0, 0, 255, 255] }
]
};
const rgba = glyph(font, glyphId)
?.scale(2)
.rasterizeWithGradient(gradient, { width: 100, height: 100 })
.toRGBA();SDF/MSDF Rendering
toSdf()
Render as Signed Distance Field (SDF) for GPU text rendering.
toSdf(options: SdfOptions): BitmapBuilderOptions:
interface SdfOptions {
width: number;
height: number;
scale: number;
offsetX?: number;
offsetY?: number;
flipY?: boolean;
spread?: number; // Default: 8
}Example:
const sdf = glyph(font, glyphId)
?.toSdf({ width: 64, height: 64, scale: 0.05, spread: 4 })
.toGray();toSdfAuto()
Render as SDF with auto-computed size from bounds.
toSdfAuto(options?: {
padding?: number;
scale?: number;
spread?: number;
flipY?: boolean;
}): BitmapBuildertoMsdf()
Render as Multi-channel Signed Distance Field (MSDF) for sharper GPU text.
toMsdf(options: MsdfOptions): BitmapBuilderExample:
const msdf = glyph(font, glyphId)
?.toMsdf({ width: 64, height: 64, scale: 0.05, spread: 4 })
.toRGBA();toMsdfAuto()
Render as MSDF with auto-computed size from bounds.
toMsdfAuto(options?: {
padding?: number;
scale?: number;
spread?: number;
flipY?: boolean;
}): BitmapBuilderAsymmetric Stroke
strokeAsymmetric()
Stroke with independent X/Y border widths. Returns both outer and inner paths.
strokeAsymmetric(options: AsymmetricStrokeOptions): {
outer: PathBuilder;
inner: PathBuilder;
}Options:
interface AsymmetricStrokeOptions {
xBorder: number;
yBorder: number;
eps?: number;
lineJoin?: "miter" | "round" | "bevel";
miterLimit?: number;
}Example:
const { outer, inner } = glyph(font, glyphId)
?.strokeAsymmetric({ xBorder: 10, yBorder: 5 });
// Use outer for border effect
const border = outer?.rasterizeAuto().toRGBA();strokeAsymmetricCombined()
Stroke with asymmetric widths, combining inner and outer into one fillable path.
strokeAsymmetricCombined(options: AsymmetricStrokeOptions): PathBuilderOutput Methods
toSVG()
Convert to SVG path data string.
toSVG(options?: SVGOptions): stringtoSVGElement()
Convert to complete SVG element.
toSVGElement(options?: SVGElementOptions): stringOptions:
interface SVGElementOptions {
fontSize?: number;
fill?: string;
stroke?: string;
strokeWidth?: number;
}toCanvas()
Render to canvas context.
toCanvas(ctx: CanvasRenderingContext2D, options?: CanvasOptions): voidOptions:
interface CanvasOptions {
flipY?: boolean;
scale?: number;
offsetX?: number;
offsetY?: number;
fill?: string;
stroke?: string;
strokeWidth?: number;
}toPath2D()
Get Path2D object for canvas.
toPath2D(options?: { flipY?: boolean; scale?: number }): Path2DtoPath()
Extract the raw GlyphPath (with transforms applied).
toPath(): GlyphPathclone()
Clone this builder.
clone(): PathBuilderBitmapBuilder
The BitmapBuilder class provides fluent methods for bitmap manipulation.
Static Factory Methods
BitmapBuilder.fromBitmap()
Create from existing bitmap.
static fromBitmap(bitmap: Bitmap): BitmapBuilderBitmapBuilder.fromBitmapWithBearing()
Create from bitmap with bearing info (used for glyph positioning).
static fromBitmapWithBearing(bitmap: Bitmap, bearingX: number, bearingY: number): BitmapBuilderBitmapBuilder.fromRasterizedGlyph()
Create from a rasterized glyph result.
static fromRasterizedGlyph(glyph: RasterizedGlyph): BitmapBuilderExample:
const glyph = rasterizeGlyph(font, glyphId, options);
const builder = BitmapBuilder.fromRasterizedGlyph(glyph);
const rgba = builder.blur(3).toRGBA();BitmapBuilder.create()
Create empty bitmap.
static create(width: number, height: number, pixelMode?: PixelMode): BitmapBuilderExample:
const canvas = BitmapBuilder.create(100, 100, PixelMode.Gray);BitmapBuilder.fromGradient()
Create a gradient bitmap.
static fromGradient(width: number, height: number, gradient: Gradient): BitmapBuilderExample:
const gradientBg = BitmapBuilder.fromGradient(100, 100, {
type: "linear",
x0: 0, y0: 0,
x1: 100, y1: 100,
stops: [
{ offset: 0, color: [255, 0, 0, 255] },
{ offset: 1, color: [0, 0, 255, 255] }
]
});Blur Effects
blur()
Gaussian blur.
blur(radius: number): BitmapBuilderboxBlur()
Box blur (faster, less smooth).
boxBlur(radius: number): BitmapBuildercascadeBlur()
Cascade blur (fast for large radii, O(1) per pixel).
cascadeBlur(radiusX: number, radiusY?: number): BitmapBuilderadaptiveBlur()
Adaptive blur (auto-selects best algorithm).
adaptiveBlur(radiusX: number, radiusY?: number): BitmapBuilderfastBlur()
Fast Gaussian blur using cascade algorithm. Recommended for large radii (> 3 pixels).
fastBlur(radius: number): BitmapBuilderExample:
// For large blur radii, fastBlur is more efficient
const blurred = glyph(font, glyphId)
?.rasterizeAuto({ padding: 50 })
.fastBlur(20) // Much faster than blur(20)
.toRGBA();Transform Effects
embolden()
Embolden (dilate) bitmap.
embolden(xStrength: number, yStrength?: number): BitmapBuildershift()
Shift bitmap position.
shift(dx: number, dy: number): BitmapBuilderresize()
Resize with nearest-neighbor interpolation.
resize(width: number, height: number): BitmapBuilderresizeBilinear()
Resize with bilinear interpolation.
resizeBilinear(width: number, height: number): BitmapBuilderpad()
Pad bitmap with empty space.
pad(left: number, top: number, right: number, bottom: number): BitmapBuilder
pad(all: number): BitmapBuilderCompositing
blend()
Alpha blend another bitmap at position.
blend(other: BitmapBuilder | Bitmap, x: number, y: number, opacity?: number): BitmapBuildercomposite()
Composite using Porter-Duff "over" operation.
composite(other: BitmapBuilder | Bitmap, x?: number, y?: number): BitmapBuilderadd()
Additive blend.
add(other: BitmapBuilder | Bitmap, x?: number, y?: number): BitmapBuildersubtract()
Subtractive blend.
subtract(other: BitmapBuilder | Bitmap, x?: number, y?: number): BitmapBuildermultiply()
Multiplicative blend.
multiply(other: BitmapBuilder | Bitmap, x?: number, y?: number): BitmapBuildermax()
Maximum blend.
max(other: BitmapBuilder | Bitmap, x?: number, y?: number): BitmapBuilderOutput Methods
convert()
Convert to different pixel mode.
convert(targetMode: PixelMode): BitmapBuildertoRGBA()
Get RGBA pixel array.
toRGBA(): Uint8ArraytoGray()
Get grayscale array.
toGray(): Uint8ArraytoBitmap()
Get raw bitmap (cloned).
toBitmap(): BitmaptoRasterizedGlyph()
Get bitmap with bearing info.
toRasterizedGlyph(): { bitmap: Bitmap; bearingX: number; bearingY: number }clone()
Clone this builder.
clone(): BitmapBuilderAccessors
get width(): number
get height(): number
get pixelMode(): PixelMode
get bearingX(): number
get bearingY(): numberPipe Function
The pipe() function enables functional composition.
function pipe<A>(a: A): A
function pipe<A, B>(a: A, ab: (a: A) => B): B
function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C
// ... up to 8 functionsPipe Operators
All pipe operators are exported with a $ prefix from the main module to avoid naming conflicts.
Path Source
$fromGlyph(font: Font, glyphId: GlyphId): GlyphPath | nullExample:
const path = $fromGlyph(font, glyphId);
if (path) {
const rgba = pipe(path, $scale(2), $rasterizeAuto(), $toRGBA);
}Path Operators
$scale(sx: number, sy?: number): (path: GlyphPath) => GlyphPath
$translate(dx: number, dy: number): (path: GlyphPath) => GlyphPath
$rotate(angle: number): (path: GlyphPath) => GlyphPath
$rotateDeg(angleDeg: number): (path: GlyphPath) => GlyphPath
$shear(shearX: number, shearY: number): (path: GlyphPath) => GlyphPath
$italic(angleDeg: number): (path: GlyphPath) => GlyphPath
$matrix(m: Matrix2D): (path: GlyphPath) => GlyphPath
$perspective(m: Matrix3x3): (path: GlyphPath) => GlyphPath
$emboldenPath(strength: number): (path: GlyphPath) => GlyphPath
$condensePath(factor: number): (path: GlyphPath) => GlyphPath
$obliquePath(slant: number): (path: GlyphPath) => GlyphPath
$strokePath(options: StrokerOptions): (path: GlyphPath) => GlyphPath
$strokeAsymmetric(options: AsymmetricStrokeOptions): (path: GlyphPath) => { outer: GlyphPath; inner: GlyphPath }
$strokeAsymmetricCombined(options: AsymmetricStrokeOptions): (path: GlyphPath) => GlyphPath
$clone(): (path: GlyphPath) => GlyphPathRasterization Operators
$rasterize(options: RasterizeOptions): (path: GlyphPath) => Bitmap
$rasterizeAuto(options?: AutoRasterOptions): (path: GlyphPath) => Bitmap
$rasterizeWithGradient(gradient: Gradient, options: RasterizeOptions): (path: GlyphPath) => Bitmap
$renderSdf(options: SdfOptions): (path: GlyphPath) => Bitmap
$renderMsdf(options: MsdfOptions): (path: GlyphPath) => BitmapBitmap Operators
$blur(radius: number): (bitmap: Bitmap) => Bitmap
$boxBlur(radius: number): (bitmap: Bitmap) => Bitmap
$cascadeBlur(radiusX: number, radiusY?: number): (bitmap: Bitmap) => Bitmap
$adaptiveBlur(radiusX: number, radiusY?: number): (bitmap: Bitmap) => Bitmap
$fastBlur(radius: number): (bitmap: Bitmap) => Bitmap
$embolden(xStrength: number, yStrength?: number): (bitmap: Bitmap) => Bitmap
$shift(dx: number, dy: number): (bitmap: Bitmap) => Bitmap
$resize(width: number, height: number): (bitmap: Bitmap) => Bitmap
$resizeBilinear(width: number, height: number): (bitmap: Bitmap) => Bitmap
$pad(left: number, top: number, right: number, bottom: number): (bitmap: Bitmap) => Bitmap
$convert(targetMode: PixelMode): (bitmap: Bitmap) => BitmapOutput Operators
$toRGBA: (bitmap: Bitmap) => Uint8Array
$toGray: (bitmap: Bitmap) => Uint8Array
$toSVG(options?: SVGOptions): (path: GlyphPath) => string
$copy: (bitmap: Bitmap) => BitmapExamples
Basic Glyph Rendering
import { glyph } from "text-shaper";
// Simple: glyph -> scale -> rasterize -> RGBA
const rgba = glyph(font, glyphId)
?.scale(2)
.rasterizeAuto({ padding: 2 })
.toRGBA();
// With rotation and blur
const blurred = glyph(font, glyphId)
?.scale(2)
.rotateDeg(15)
.rasterizeAuto({ padding: 10 })
.blur(3)
.toRGBA();Text Effects
// Synthetic bold italic
const boldItalic = glyph(font, glyphId)
?.embolden(50) // Path-level bold
.italic(12) // 12-degree italic
.scale(2)
.rasterize({ width: 100, height: 100 })
.embolden(1, 1) // Bitmap-level additional bold
.toRGBA();
// Stroked text
const stroked = glyph(font, glyphId)
?.stroke({ width: 20, lineCap: "round", lineJoin: "round" })
.scale(2)
.rasterizeAuto()
.toRGBA();Shadow/Glow Effect
const glyphPath = glyph(font, glyphId)?.scale(2);
if (!glyphPath) throw new Error("Glyph not found");
// Create shadow
const shadow = glyphPath
.clone()
.translate(4, 4)
.rasterizeAuto({ padding: 20 })
.cascadeBlur(8, 8);
// Create main glyph
const main = glyphPath.rasterizeAuto({ padding: 20 });
// Composite shadow + main
const result = shadow.composite(main).toRGBA();Canvas Rendering
const path = glyph(font, glyphId)?.scale(2);
if (path) {
path.toCanvas(ctx, {
offsetX: 100,
offsetY: 200,
fill: "black"
});
}Combining Multiple Glyphs
import { glyph, combine } from "text-shaper";
const h = glyph(font, font.glyphId("H".charCodeAt(0))!)?.translate(0, 0);
const i = glyph(font, font.glyphId("i".charCodeAt(0))!)?.translate(50, 0);
if (h && i) {
const combined = combine(h, i)
.scale(2)
.rasterizeAuto()
.toRGBA();
}Using Pipe Style
import { pipe, $scale, $rotate, $rasterizeAuto, $blur, $toRGBA, getGlyphPath } from "text-shaper";
const path = getGlyphPath(font, glyphId);
if (path) {
const rgba = pipe(
path,
$scale(2, 2),
$rotate(Math.PI / 4),
$rasterizeAuto({ padding: 5 }),
$blur(3),
$toRGBA
);
}Lazy vs Eager Transforms
// Lazy: transforms accumulated, applied at render
const lazy = glyph(font, glyphId)
?.scale(2) // Matrix multiply
.rotate(0.5) // Matrix multiply
.translate(10, 10) // Matrix multiply
.rasterizeAuto(); // Apply combined matrix once
// Force immediate application with .apply()
const eager = glyph(font, glyphId)
?.scale(2)
.apply() // Apply transforms now
.embolden(50) // Embolden the scaled path
.rotate(0.5) // New lazy transform
.rasterizeAuto();Additional Features
Atlas Building
Build texture atlases for efficient GPU rendering.
import { buildAtlas, buildAsciiAtlas, buildStringAtlas, atlasToRGBA, getGlyphUV } from "text-shaper";
// Build atlas for ASCII characters
const atlas = buildAsciiAtlas(font, { fontSize: 32, padding: 2 });
// Build atlas for specific glyphs
const customAtlas = buildAtlas(font, [glyphId1, glyphId2], { fontSize: 32 });
// Build atlas for specific text
const textAtlas = buildStringAtlas(font, "Hello World", { fontSize: 32 });
// Get UV coordinates for rendering
const uv = getGlyphUV(atlas, glyphId);
// { u0, v0, u1, v1 }
// Convert to RGBA for GPU upload
const rgba = atlasToRGBA(atlas);MSDF Atlas Building
Build multi-channel signed distance field atlases for scalable GPU text.
import { buildMsdfAtlas, buildMsdfAsciiAtlas, msdfAtlasToRGBA } from "text-shaper";
// Build MSDF atlas for ASCII
const msdfAtlas = buildMsdfAsciiAtlas(font, { fontSize: 32, spread: 4 });
// Convert to RGBA for GPU upload
const msdfRgba = msdfAtlasToRGBA(msdfAtlas);Direct Glyph Rasterization
Rasterize glyphs without building a PathBuilder.
import { rasterizeGlyph, rasterizeText } from "text-shaper";
// Rasterize single glyph
const result = rasterizeGlyph(font, glyphId, 24);
if (result) {
const { bitmap, bearingX, bearingY } = result;
}
// Rasterize text string (multiple glyphs)
const textResult = rasterizeText(font, "Hello", 24);Shaped Text Rendering
Render shaped text to canvas or SVG.
import { shape, renderShapedText, shapedTextToSVG, glyphBufferToShapedGlyphs } from "text-shaper";
// Shape text
const buffer = new UnicodeBuffer(text);
shape(font, buffer);
// Convert to shaped glyphs
const shapedGlyphs = glyphBufferToShapedGlyphs(buffer);
// Render to canvas
renderShapedText(ctx, font, shapedGlyphs, {
fontSize: 24,
fill: "black"
});
// Convert to SVG
const svg = shapedTextToSVG(font, shapedGlyphs, {
fontSize: 24,
fill: "currentColor"
});SDF/MSDF for GPU Rendering
Generate signed distance fields for scalable GPU text.
// Single glyph SDF
const sdf = glyph(font, glyphId)
?.toSdfAuto({ spread: 8, scale: 0.1 })
.toGray();
// Single glyph MSDF (better quality)
const msdf = glyph(font, glyphId)
?.toMsdfAuto({ spread: 8, scale: 0.1 })
.toRGBA();Asymmetric Stroke Effects
Create directional borders and shadow effects.
// Horizontal drop shadow
const { outer } = glyph(font, glyphId)
?.strokeAsymmetric({ xBorder: 5, yBorder: 0 });
const shadow = outer?.translate(2, 2).rasterizeAuto().blur(3);
// Vertical stretch effect
const stretched = glyph(font, glyphId)
?.strokeAsymmetricCombined({ xBorder: 0, yBorder: 10 })
.rasterizeAuto()
.toRGBA();