utils

Source: utils.js:26

Pure helpers shared across the codebase and exposed to plugin authors via window.gstmxx. Three layers, top to bottom:

  1. Math primitives — distance, avgPoint, lerp, scaleFrom, point. Trivial geometric building blocks. Pure functions, no DOM.
  2. Canvas drawing primitives — drawClosedPath, drawOpenPath, drawLabel, roundRect. Wrappers around CanvasRenderingContext2D that codify the project's visual language (label background, dashed lines, mirrored-text handling).
  3. 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 as a.

Returns

  • number — Euclidean distance, or Infinity if 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, or null to skip.
  • strokeStyle (string | null, optional, default: null) — Stroke color, or null to 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) — When true, 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

  • booleantrue when clip has been applied, false when 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

  • booleantrue when clip has been applied, false when 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

  • booleantrue when clip has been applied, false when 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

  • booleantrue when clip has been applied, false when 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.