Writing a renderer
The DOM driver in @texmorph/dom works with any object that implements FormulaRenderer. The KaTeX and MathJax packages are two such implementations.
ts
import type { FormulaRenderer } from '@texmorph/dom';
const renderer: FormulaRenderer<MyOptions> = {
name: 'my-renderer',
version: '1.0.0',
async ready(signal) { /* wait for fonts or other resources */ },
async render(container, state, opts, signal) { /* render state.latex into container and measure it */ },
async adopt(el, signal) { /* measure existing markup, or return null */ },
resnapshot(rendered) { /* measure again; used by refresh() */ },
maskTokens(rendered) { /* hide token elements; return a function that restores them */ },
createGhostLayer(stage, bounds) { /* an element ghosts are placed in */ },
createGhost(token, rendered) { /* an unpositioned copy of one token */ },
stylesheet() { /* optional: CSS for shadow roots */ },
async loadShapes(signal) { /* optional: load what outline morphing needs */ },
createShapeGhost(source, target, track) { /* optional: one outline morph between two ghosts, or null */ },
watchFrameRate(minFps, onSlow) { /* optional: frame-rate probe for shapes: 'auto' */ },
};Then use it with createMorphWith(renderer, container, from, to, options), or pass it to any integration.
Requirements
- Snapshots.
render,adoptandresnapshotreturn aRenderedFormulawhosesnapshotlists tokens with dense indices: text tokens first in reading order, then structure tokens. Boxes are relative to the formula root, in CSS pixels, and corrected for any CSS transform on ancestors. - visualKey. Two tokens with equal
visualKeymust look identical when one is scaled to the other's box. Include anything that changes the drawing, such as font, weight, variant, or the path of a stretched shape. - Failure. When a formula cannot be measured, return an empty snapshot with an
errordiagnostic and leave the root in place; the driver then crossfades. Do not report an error for a legitimately empty formula. - Ghosts.
createGhostmust look the token up bytoken.indexin the renderer's own data; it must not trust the box of the token object it receives. - Ghost layer writes.
GhostLayer.place()may set a ghost's size and transform origin when they change, which in practice happens once during prepare. Every later call may write only position (left/topfor HTML ghosts), transform, opacity and color. Rendering must never read layout. - Pixel grid. At
t = 0andt = 1the real formula is shown; just after and before, the ghosts take over. Ghosts must land on exactly the pixels the real tokens occupy, or the formula visibly shifts by a fraction of a pixel on high-DPI screens. The HTML layer positions ghosts withleft/topand drops the transform while a ghost is unscaled, so text follows the same pixel snapping as the real formula. The MathJax layer moves the formula<svg>and its ghost-layer<svg>onto whole pixels. - Outline morphing (optional). For a matched track whose glyphs differ, the driver adds both ghosts to the layer, then calls
createShapeGhost(source, target, track). A returnedShapeGhostreplaces the two ghosts: the driver hides them, addselto the layer and places it like the source ghost at scale 1, after callingdraw(mix)with the eased progress (0 = source glyph, 1 = target glyph; it can leave that range with overshooting easings). The outline is drawn in the source ghost's coordinates, so atmix = 1it must cover the target glyph as placed at the source ghost's origin. Return null to keep stretch-and-crossfade for that track.loadShapesis awaited once per morph before anycreateShapeGhost; if it rejects, the morph reportsshape/unavailableand crossfades. - Frame-rate probe (optional). Core and dom own no clock, so
shapes: 'auto'asks the renderer to measure. The driver callswatch.touch()on every frame it draws between the endpoints andwatch.stop()on dispose; the renderer callsonSlow(fps)once if playback stays belowminFps, and the driver then switches to stretch-and-crossfade. WithoutwatchFrameRate,'auto'behaves like'always'. - Stage. Ghost-layer coordinates are relative to the stage element passed to
createGhostLayer. For HTML ghosts,createHtmlGhostLayerandplaceHtmlGhostfrom@texmorph/domimplement all of this.
The @texmorph/core API documents the snapshot types.