utils
Source: utils.js:26
Pure helpers shared across the codebase and exposed to plugin authors via window.gstmxx. Three layers, top to bottom:
- Math primitives —
distance,avgPoint,lerp,scaleFrom,point. Trivial geometric building blocks. Pure functions, no DOM. - Canvas drawing primitives —
drawClosedPath,drawOpenPath,drawLabel,roundRect. Wrappers aroundCanvasRenderingContext2Dthat codify the project's visual language (label background, dashed lines, mirrored-text handling). - Composed makeup helpers —
expandEyePolygon,drawEyeWing,drawCheekSweep,drawContourBand. Built from the lower layers, these are what most 2D ghostyle plugins reach for.
Plus the in-app logging helpers (formatTime, setLog, updateLogDisplay) which write to state.logsArchive and rerender the log box.
Why this module exists as the public plugin API surface: plugin authors should never need to know about face-api or MediaPipe internals — they should just Ghostati.drawClosedPath(ctx, points, …) and have it look consistent with everything else. Keeping the helpers here (and pure) makes them unit-testable without a browser.
Static Methods
distance(a: Array.<number>, b: Array.<number>): number
Euclidean distance between two equal-length numeric arrays. Returns Infinity for malformed input rather than throwing, because the callers are inference hot paths that prefer a degraded comparison over an exception (an Infinity distance simply never wins a min/closest query).
Parameters
a(Array.<number>) — First vector.b(Array.<number>) — Second vector, same length asa.
Returns
number— Euclidean distance, orInfinityif inputs are invalid.
- See:
- scripts/engine.js – `seekFaceInDb()` and `computeCompositeMetrics()` use this against 128-D face-api descriptors.
avgPoint(points: Array.<{x:number, y:number}>): Object
Centroid of an array of 2D points. Plugin authors use it to anchor labels or shapes at the visual centre of a feature (eye, mouth) without having to reduce manually.
Parameters
points(Array.<{x:number, y:number}>) — Non-empty array of points.
Returns
Object— The averaged point.
- See:
- expandEyePolygon – uses this to locate the eye centre.
- ghostyles/smokey-eyes.js, ghostyles/lip-tint.js – representative plugin consumers via `Ghostati.avgPoint`.
lerp(a: Object, b: Object, t: number): Object
Linear interpolation between two 2D points. t = 0 returns a, t = 1 returns b; values outside [0, 1] extrapolate, which is sometimes useful for "slightly past the endpoint" effects (the splash.js plugin does this with t = 1.2 and t = -0.6 for off-edge anchors).
Parameters
a(Object) — Start point.b(Object) — End point.t(number) — Interpolation factor; clamped only by the caller.
Returns
Object— The interpolated point.
- See:
- expandEyePolygon, drawEyeWing, drawCheekSweep – internal uses.
- ghostyles/splash.js – extensive use via `Ghostati.lerp` for anchors.
scaleFrom(center: Object, point: Object, scale: number): Object
Scale a point outward from a centre by a uniform factor. scale > 1 pushes the point away from the centre; scale < 1 pulls it in. Used to grow or shrink a feature polygon while keeping its centroid fixed.
Parameters
center(Object) — Pivot of the scaling.point(Object) — Point to scale.scale(number) — Multiplicative factor.
Returns
Object— The scaled point.
- See:
- expandEyePolygon – the canonical caller, used to enlarge eye outlines.
point(x: number, y: number): Object
Convenience constructor for a {x, y} point. Exists so plugin code reads fluently (Ghostati.point(120, 40) rather than {x: 120, y: 40}) when inline-building anchor positions or pushing extra points onto a path.
Parameters
x(number)y(number)
Returns
Object
- See:
- ghostyles/smokey-eyes.js, ghostyles/stage-mask.js – use this when appending custom anchors to landmark-derived polygons.
drawClosedPath(
ctx: CanvasRenderingContext2D,
points: Array.<{x:number, y:number}>,
fillStyle?: string | null,
strokeStyle?: string | null,
lineWidth?: number,
)
Stroke and/or fill a closed polygon defined by an array of points. Either fillStyle or strokeStyle can be null to skip that operation, so the same function works for filled shapes, outlines only, or both. No-ops silently on an empty points array.
Parameters
ctx(CanvasRenderingContext2D) — Target canvas context.points(Array.<{x:number, y:number}>) — Polygon vertices.fillStyle(string | null, optional, default: null) — Fill color, ornullto skip.strokeStyle(string | null, optional, default: null) — Stroke color, ornullto skip.lineWidth(number, optional, default: 2) — Stroke width (ignored if no stroke).
- See:
- drawEyeWing – fills and strokes the eye shape via this.
- drawCheekSweep – fills the cheek polygon.
- drawContourBand – uses `drawOpenPath` instead (this is for closed shapes only).
drawOpenPath(
ctx: CanvasRenderingContext2D,
points: Array.<{x:number, y:number}>,
strokeStyle: string,
lineWidth?: number,
dashed?: boolean,
)
Stroke an open polyline through the given points. Supports an optional dashed style for the "graphical liner" aesthetic some plugins use. Wraps ctx.save/ctx.restore so the dash pattern doesn't leak into subsequent drawing.
Parameters
ctx(CanvasRenderingContext2D)points(Array.<{x:number, y:number}>)strokeStyle(string) — Stroke colour.lineWidth(number, optional, default: 2)dashed(boolean, optional, default: false) — Whentrue, applies a[10, 8]dash pattern.
- See:
- drawEyeWing – uses this for the eyeline strokes.
- drawContourBand – uses this twice (dashed + solid) for layered bands.
drawLabel(
ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
)
Draw a small labelled box at (x, y). The label background is a rounded dark rectangle with a thin outline; the text is rendered in the project-standard sans-serif. When the canvas is mirrored (state.isMirrored) the label is un-mirrored locally so the text remains readable left-to-right.
Parameters
ctx(CanvasRenderingContext2D)text(string) — Label text.x(number) — Left anchor (the box grows rightward from here).y(number) — Bottom-of-label baseline.
- See:
- drawEyeWing, drawCheekSweep, drawContourBand – call this to caption the corresponding region.
- roundRect – used internally to draw the background pill.
roundRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
r: number,
)
Append a rounded-rectangle subpath to the current canvas path. Caller is responsible for ctx.fill() / ctx.stroke() afterwards. The corners are drawn with arcTo so the radius is honoured exactly even on rectangles whose dimensions approach 2 * r.
Parameters
ctx(CanvasRenderingContext2D)x(number) — Top-left x.y(number) — Top-left y.w(number) — Width.h(number) — Height.r(number) — Corner radius.
- See:
- drawLabel – the only caller in-tree; uses this for the label pill.
expandEyePolygon(
eye: Array.<{x:number, y:number}>,
eyebrow: Array.<{x:number, y:number}>,
scale?: number,
eyebrowLift?: number,
): Array.<{x:number, y:number}>
Build an enlarged eye-shape polygon by combining the upper eyebrow points (lifted toward the eye via lerp) with the lower eye points (radially expanded via scaleFrom). The output is an ordered ring of points suitable for drawClosedPath.
The two tuning factors are intentionally exposed so plugins can dial the dramatic level: scale controls how far the lower lid bulges out, eyebrowLift controls how close the top of the shape sits to the eye (0 = right on the brow, 1 = right at the eye).
Parameters
eye(Array.<{x:number, y:number}>) — Six face-api eye landmarks.eyebrow(Array.<{x:number, y:number}>) — Five face-api eyebrow landmarks.scale(number, optional, default: 1.22) — Outward scaling factor of the lower lid.eyebrowLift(number, optional, default: 0.72) — Lerp factor between eye and eyebrow.
Returns
Array.<{x:number, y:number}>— Ring of points defining the eye region.
- See:
- drawEyeWing – the primary caller.
- avgPoint, scaleFrom, lerp – primitives composed here.
drawEyeWing(
ctx: CanvasRenderingContext2D,
eye: Array.<{x:number, y:number}>,
eyebrow: Array.<{x:number, y:number}>,
label: string,
tone: Object,
)
Draw a stylised winged-eye shape with a tail extending past the outer corner, the underlying enlarged-eye polygon, and a label. Side-aware: the tone.side argument ('left' or 'right') flips which eye corner becomes the tail anchor and on which side the label sits.
tone collects every styling decision so the function stays generic across different plugins: scale of the polygon, eyebrow lift, fill / stroke / line colours, the tail offsets, and the side. Plugins typically pass a literal object inline; see graphic-liner.js and splash.js for usage examples.
Parameters
ctx(CanvasRenderingContext2D)eye(Array.<{x:number, y:number}>) — Six face-api eye landmarks.eyebrow(Array.<{x:number, y:number}>) — Five face-api eyebrow landmarks.label(string) — Label drawn near the tail.tone(Object) — Styling and geometry knobs.
- See:
- expandEyePolygon – called first to compute the base shape.
- drawClosedPath, drawOpenPath, drawLabel – the rendering primitives.
drawCheekSweep(
ctx: CanvasRenderingContext2D,
anchor: Object,
noseSide: Object,
mouthCorner: Object,
jawPoint: Object,
label: string,
fill: string,
stroke: string,
)
Draw a cheek-sweep shape: a six-vertex polygon traced from an upper anchor down to the jawline and back through the mouth corner. Internally interpolates intermediate vertices with lerp so the curve feels organic rather than landmark-blocky.
Parameters
ctx(CanvasRenderingContext2D)anchor(Object) — Upper anchor (typically the outer eye corner).noseSide(Object) — Side-of-nose anchor.mouthCorner(Object) — Outer mouth corner.jawPoint(Object) — Lower jaw anchor.label(string)fill(string) — Fill colour.stroke(string) — Stroke colour.
- See:
- drawClosedPath – used to render the polygon.
- drawLabel – captions the swept region.
- lerp – used to compute intermediate vertices.
drawContourBand(
ctx: CanvasRenderingContext2D,
pts: Array.<{x:number, y:number}>,
label: string,
)
Draw a "contour band" along an arbitrary polyline: two stacked drawOpenPath strokes, one dashed-warm and one wider translucent-dark, with a label anchored at the midpoint. The double-stroke gives a makeup-contour aesthetic on top of an open landmark sequence (jawline, nose ridge).
Parameters
ctx(CanvasRenderingContext2D)pts(Array.<{x:number, y:number}>) — Polyline points (e.g. a jawline slice).label(string)
- See:
- drawOpenPath – called twice for the layered stroke effect.
- drawLabel – places the caption at the midpoint of the band.
clipLeftHalf(
ctx: CanvasRenderingContext2D,
landmarks: object | Array.<{x:number, y:number}>,
): boolean
Clip the current canvas context to the left half of a face-api landmark cloud.
The split axis is the vertical line that passes through the landmarks centroid. This helper exists so a plugin can render a side-by-side comparison where one half of the face shows a 2D pixel-space strategy and the other half is left untouched (or rendered by a different callback).
IMPORTANT: call ctx.save() before using this helper and ctx.restore() afterwards. The function only applies the clip path.
Parameters
ctx(CanvasRenderingContext2D) — 2D rendering context.landmarks(object | Array.<{x:number, y:number}>) — face-api landmarks object or array of points.
Returns
boolean—truewhen clip has been applied,falsewhen landmarks are missing.
- See:
- ghostyles/smokey-eyes.js
- ghostyles/soft-contour.js
clipRightHalf(
ctx: CanvasRenderingContext2D,
landmarks: object | Array.<{x:number, y:number}>,
): boolean
Clip the current canvas context to the right half of a face-api landmark cloud.
The split axis is the vertical line through the landmarks centroid. Use this together with clipLeftHalf when a plugin needs to compare two rendering strategies on the same face without visual overlap.
IMPORTANT: call ctx.save() before using this helper and ctx.restore() afterwards. The function only applies the clip path.
Parameters
ctx(CanvasRenderingContext2D) — 2D rendering context.landmarks(object | Array.<{x:number, y:number}>) — face-api landmarks object or array of points.
Returns
boolean—truewhen clip has been applied,falsewhen landmarks are missing.
- See:
- ghostyles/smokey-eyes.js
- ghostyles/soft-contour.js
clipLeftHalfUV(
ctx: CanvasRenderingContext2D,
landmarks3d: Array.<{x:number, y:number, z: ?number}>,
): boolean
Clip a UV canvas to the left half of the MediaPipe landmark cloud.
The landmarks are normalized in [0,1], so the centroid x is converted to pixel coordinates of the UV canvas before clipping. This makes it possible to show UV-space rendering only on one side while another strategy occupies the opposite side.
IMPORTANT: call ctx.save() before using this helper and ctx.restore() afterwards. The function only applies the clip path.
Parameters
ctx(CanvasRenderingContext2D) — UV texture rendering context.landmarks3d(Array.<{x:number, y:number, z: ?number}>) — MediaPipe face landmarks.
Returns
boolean—truewhen clip has been applied,falsewhen landmarks are missing.
- See:
- ghostyles/smokey-eyes.js
- ghostyles/soft-contour.js
clipRightHalfUV(
ctx: CanvasRenderingContext2D,
landmarks3d: Array.<{x:number, y:number, z: ?number}>,
): boolean
Clip a UV canvas to the right half of the MediaPipe landmark cloud.
This is the UV-space counterpart of clipRightHalf. It supports split-view plugins where one side is painted in pixel-space and the other side in UV-space, keeping the didactic comparison readable.
IMPORTANT: call ctx.save() before using this helper and ctx.restore() afterwards. The function only applies the clip path.
Parameters
ctx(CanvasRenderingContext2D) — UV texture rendering context.landmarks3d(Array.<{x:number, y:number, z: ?number}>) — MediaPipe face landmarks.
Returns
boolean—truewhen clip has been applied,falsewhen landmarks are missing.
- See:
- ghostyles/smokey-eyes.js
- ghostyles/soft-contour.js
formatTime(): string
Current wall-clock time as a HH:MM:SS string. Used exclusively for log timestamping so the workshop participants can correlate UI events with what they did.
Returns
string
- See:
- <p>setLog – prefixes every log line with <code>[formatTime()]</code>.</p>
formatRelativeTime(
dateLike: Date | string | number | null | undefined,
): string
Convert a date-like value into a compact Italian relative-time label. Used by plugin buttons to show freshness metadata ("aggiornato X fa").
Parameters
dateLike(Date | string | number | null | undefined)
Returns
string— Relative time label (e.g. "ora", "17 minuti fa", "2 mesi fa") or "n/d" when input is invalid.
updateLogDisplay()
Re-render the on-screen log box from state.logsArchive. Has two modes: expanded (full archive, oldest at top, autoscrolled to bottom — for post-mortem) and collapsed (latest four lines, newest at top — for live peripheral attention). Mode is driven by state.isLogExpanded, toggled by clicking the log box.
- See:
- setLog – calls this after pushing a new entry.
- scripts/main.js – wires the click handler that toggles `state.isLogExpanded`.
setLog(message: string, sourcePlugin?: string | null)
Append a log line to state.logsArchive and refresh the on-screen log. Each line is timestamped via formatTime() and may carry an optional source-plugin tag (rendered in accent colour) so a workshop participant can tell at a glance which plugin emitted the message. The archive is capped at 100 entries (FIFO drop); visibleLogStartIndex shifts in step so the "clear visible logs" feature never accidentally reveals stale data.
Parameters
message(string) — Text of the log entry.sourcePlugin(string | null, optional, default: null) — Optional plugin tag.
- See:
- formatTime – produces the timestamp prefix.
- updateLogDisplay – refreshes the DOM after the push.
asErrorLabel(err: *): string
Converts thrown plugin failures into the compact label used by loader logs. In this project it keeps broken paintUV errors readable while preserving non-Error throws from third-party ghostyles.
Parameters
err(*) — Value caught while rendering a 3D ghostyle.
Returns
string— Error name and message, or a stringified thrown value.
- See:
- deactivateBroken3dPlugin - Formats the `paintUV` failure logged for a disabled plugin.
syncSize(
canvas: HTMLCanvasElement,
overlayEl: HTMLCanvasElement,
): void
Keeps the hidden 3D plugin canvas matched to the live overlay dimensions so UV paint is composited into the same coordinate space as the camera frame.
Parameters
canvas(HTMLCanvasElement) — The hidden 3D plugin canvas.overlayEl(HTMLCanvasElement) — The main overlay canvas.
Returns
void
- See:
- initPlugins3dLoader - Called on each `landmarks3d` frame before drawing the active ghostyle.
syncMirror(
canvas: HTMLCanvasElement,
overlayEl: HTMLCanvasElement,
): void
Mirrors the 3D plugin canvas with the main overlay transform, preserving the webcam mirror mode when UV-painted effects are displayed or composited.
Parameters
canvas(HTMLCanvasElement) — The hidden 3D plugin canvas.overlayEl(HTMLCanvasElement) — The main overlay canvas.
Returns
void
- See:
- initPlugins3dLoader - Called on each `landmarks3d` frame before drawing the active ghostyle.