Pretext Skill for Hermes Agent: DOM-Free Text Layout Demos
Build creative browser demos with DOM-free text layout.
Written by Neura Market from the official Hermes Agent documentation for Pretext. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationPretext is a small TypeScript library that measures and lays out multiline text without touching the DOM. The Hermes Agent skill of the same name wraps it so the agent can produce self-contained browser demos where text is the raw material: paragraphs that part around a dragged sprite, games built from words, kinetic typography, ASCII effects made from real prose. Reach for it when someone asks for a "pretext demo" or any visual piece where line breaks must be known before anything is drawn.
What it does
The library, published by Cheng Lou, answers one question: given a string, a font, and a width, where do the lines break, how wide is each line, where does each grapheme sit, and how tall is the block? It computes all of that through canvas measurement, so no browser reflow is triggered. That narrow contract is what makes it useful as a creative primitive. Because the numbers come back fast and geometrically, you can reflow a paragraph around a moving object every frame, treat measured words as collision rectangles in a game, or give each visible character an exact starting coordinate for a particle effect.
The skill exists to turn that primitive into finished demos rather than plumbing snippets. It carries templates, a pattern catalogue, a workflow, and a list of pitfalls that the agent follows when building a single-file HTML piece.
Before you start
This is an optional skill, installed on demand rather than shipped by default. Its path is optional-skills/creative/pretext, version 1.0.0, authored by Hermes Agent under the MIT licence. It runs on linux, macos, and windows.
The demos themselves are single self-contained HTML files with no build step. The only runtime dependency is @chenglou/pretext, loaded from the esm.sh CDN, plus the browser's built-in Intl.Segmenter for grapheme splitting. Nothing needs to be installed locally beyond a way to serve a file over HTTP for testing.
The stack
Each demo is one HTML file. The table below is the source's own layering.
| Layer | Tool | Purpose |
|---|---|---|
| Core | @chenglou/pretext via esm.sh CDN | Text measurement + line layout |
| Render | HTML5 Canvas 2D | Glyph rendering, per-frame composition |
| Segmentation | Intl.Segmenter (built-in) | Grapheme splitting for emoji / CJK / combining marks |
| Interaction | Raw DOM events | Mouse / touch / wheel, no framework |
The import block pulls in every entry point the skill uses. Pin the version: the source shows @0.0.6 and advises checking npm if demo behaviour looks off.
<script type="module">
import {
prepare, layout, // use-case 1: simple height
prepareWithSegments, layoutWithLines, // use-case 2a: fixed-width lines
layoutNextLineRange, materializeLineRange, // use-case 2b: streaming / variable width
measureLineStats, walkLineRanges, // stats without string allocation
} from "https://esm.sh/@chenglou/pretext@0.0.6";
</script>
The two use cases
Nearly every demo reduces to one of two shapes. Learn both before writing anything.
Use case 1: measure, then let CSS draw
Here you keep the browser as the renderer and ask pretext only for dimensions. The prepared handle is built once from the text and font, then layout returns the height and line count for a given width.
const prepared = prepare(text, "16px Inter");
const { height, lineCount } = layout(prepared, 320, 20);
This shape suits virtualised lists whose rows contain wrapping text, masonry grids that need exact card heights, dev-time checks for whether a label fits, and preventing layout shift when remote text arrives. The critical rule: the font and letter spacing you pass must match the CSS exactly. The canvas font string format, such as "16px Inter" or "500 17px 'JetBrains Mono'", has to correspond to what the page actually renders, or the measurements drift.
Use case 2: measure and draw yourself
This is where the creative work happens. You own the drawing, so you can render to canvas, SVG, or WebGL, apply per-glyph transforms, and treat line metadata as geometry.
const prepared = prepareWithSegments(text, FONT);
const { lines } = layoutWithLines(prepared, 320, 26);
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26);
}
For flow where each line has a different width, such as text around a shape, inside a donut band, or in a non-rectangular column, you drive the layout with a cursor and a per-row width function.
let cursor = { segmentIndex: 0, graphemeIndex: 0 };
let y = 0;
while (true) {
const lineWidth = widthAtY(y); // your function: how wide is the corridor at this y?
const range = layoutNextLineRange(prepared, cursor, lineWidth);
if (!range) break;
const line = materializeLineRange(prepared, range);
ctx.fillText(line.text, leftEdgeAtY(y), y);
cursor = range.end;
y += lineHeight;
}
The source calls this the most important pattern in the library. It is what makes text flow around a dragged sprite possible, the demo that circulated widely.
Helpers worth knowing
measureLineStats(prepared, maxWidth) returns { lineCount, maxLineWidth }, which gives you the widest line and therefore the shrink-wrap width for a multiline block. walkLineRanges(prepared, maxWidth, callback) iterates lines without allocating strings, useful for statistics or physics over graphemes when you do not need the characters. The @chenglou/pretext/rich-inline subpath offers the same system for paragraphs that mix fonts, chips, and mentions.
Demo recipe patterns
The community corpus clusters into a handful of strong patterns. The skill's advice is to pick one and riff rather than invent a new category unless the brief demands it.
| Pattern | Key API | Example idea |
|---|---|---|
| Reflow around obstacle | layoutNextLineRange + per-row width function | Editorial paragraph that parts around a dragged cursor sprite |
| Text-as-geometry game | layoutWithLines + per-line collision rects | Breakout where each brick is a measured word |
| Shatter / particles | walkLineRanges → per-grapheme (x,y) → physics | Sentence that explodes into letters on click |
| ASCII obstacle typography | layoutNextLineRange + measured per-row obstacle spans | Bitmap ASCII logo, shape morphs, and draggable wire objects that make text open around their actual geometry |
| Editorial multi-column | layoutNextLineRange per column + shared cursor | Animated magazine spread with pull quotes |
| Kinetic type | layoutWithLines + per-line transform over time | Star Wars crawl, wave, bounce, glitch |
| Multiline shrink-wrap | measureLineStats | Quote card that auto-sizes to its tightest container |
Two working single-file starters ship with the skill: templates/donut-orbit.html and templates/hello-orb-flow.html.
Workflow
- Pick a pattern from the table that matches the brief.
- Start from a template.
templates/hello-orb-flow.htmlshows text reflowing around a moving orb, the reflow-around-obstacle pattern.templates/donut-orbit.htmlis the advanced example: measured ASCII logo obstacles, a draggable wire sphere and cube, morphing shape fields, selectable DOM text, and dev-only controls. Usewrite_fileto place a new.htmlin~/.hermes/cache/scratch/or the user's workspace. - Swap the corpus for something intentional to the brief. Real prose, 10 to 100 sentences, no lorem ipsum.
- Tune the aesthetic: font, palette, composition, interaction. The source is blunt that this is the work and should not be skipped.
- Verify locally by serving the directory over HTTP.
cd <dir-with-html> && python -m http.server 8765
# then open http://localhost:8765/<file>.html
- Check the console. Pretext throws if
prepareWithSegmentsreceives a bad font string, andIntl.Segmenteris present in every modern browser. - Give the user the file path, not just the code. They want to open it.
The creative standard
Pretext returns numbers; the demo is drawn by you. The skill sets a bar for what counts as finished.
Do not ship a hello-world. hello-orb-flow.html is a starting point, and every delivered demo must add intentional colour, motion, composition, and one visual detail the user did not ask for but will appreciate. Commit to a palette: amber on black in the CRT tradition, cold white on charcoal for an editorial feel, or desaturated pastels for a risograph look. Proportional fonts are the point, since the library's whole character is that it is not monospaced. Use Iowan Old Style, Inter, JetBrains Mono, Helvetica Neue, or a variable font, never the default sans. The text itself should mean something: a short manifesto, poetry, real source code, a found text, the library's own README. Never lorem ipsum. And the first paint must look shippable, with no loading states and no blank frames.
Performance notes
prepare() and prepareWithSegments() are the expensive calls. Run them once per text and font pair and cache the handle. On resize, rerun only layout() or layoutWithLines(), never re-prepare. For per-frame animation where the text is fixed but the geometry moves, layoutNextLineRange in a tight loop is cheap enough for normal-length paragraphs at 60fps.
When rendering ASCII masks per frame, keep a cell buffer in typed arrays, derive measured per-row obstacle spans from the cells or projected geometry, merge the spans, and feed them into layoutNextLineRange before drawing. Keep visual and layout animation coupled: if a sphere morphs into a cube, tween the rendered cell buffer and the obstacle spans with the same value, or the demo looks painted on rather than physically reflowed. For fades, prefer layer opacity over changing glyph intensity or obstacle scale, putting transient ASCII sprites on their own canvas and fading that canvas with CSS or GSAP so the geometry does not appear to shrink. Finally, setting ctx.font is surprisingly slow, so set it once per frame when the font does not vary rather than before every fillText call.
Limits and gotchas
The source lists eight pitfalls, and they are worth reading before the first build.
- Drifting CSS and canvas font strings. If the measured font is
"16px Inter"but the CSS falls back to sans-serif because Inter failed to load, measurements drift by 5 to 20 percent. Preload the font or use a web-safe family. - Re-preparing inside the animation loop. Only the
layout*calls are cheap. Keep the prepared handle in module scope. - Forgetting
Intl.Segmenterfor grapheme splits. Emoji, combining marks, and CJK break naive string splitting. Usenew Intl.Segmenter(undefined, { granularity: "grapheme" })when sampling individual visible glyphs. break: 'never'chips withoutextraWidth. Inrich-inline, an atomic chip or mention also needsextraWidthfor the pill padding, or the chrome overflows the container.- Loading
@chenglou/pretextfromunpkgwhen the entry is TypeScript-only. Useesm.sh, which compiles the TS exports to browser-ready ESM.unpkgwill 404 or serve raw TS. - Monospace fallbacks erasing the point. If the output looks monospaced, the CSS
font-familyprobably fell through tomonospace. Verify the rendered font in DevTools. - Skipping rows versus shrinking width when flowing around a shape. If a row's corridor is too narrow, skip the row with
y += lineHeight; continue;rather than passing a tiny maxWidth, which produces one-grapheme lines that look broken. - Shipping a cold demo. The default first paint looks tutorial-grade. Add a vignette, a subtle scanline, idle auto-motion, and one carefully chosen interactive response such as drag, hover, scroll, or click.
Verification checklist
Before handing anything over, confirm the demo is a single self-contained .html file that opens by double-click or through python -m http.server. The import must come from esm.sh with a pinned version. The corpus must be real prose matching the concept. The font string passed to prepare must match the CSS exactly. prepare() or prepareWithSegments() must be called once, not per frame. The background should be dark with a considered palette, not the default white canvas. There should be at least one interactive response or idle auto-motion. Test locally with python -m http.server and confirm no console errors. Aim for 60fps on a mid-tier laptop, or document graceful degradation. And include one extra-mile detail the user did not ask for.
When not to use it
Skip this skill for static SVG or HTML pages where CSS already handles layout; just use CSS. It is also the wrong tool for rich text editors and general inline formatting engines, since pretext is intentionally narrow. Image-to-text work belongs to the ascii-art and ascii-video skills. Pure canvas generative art with no text role belongs to p5js.
What pairs with it
The related skills are p5js for canvas work without a text role, claude-design for design direction, excalidraw for diagrams, and architecture-diagram for structural drawings. For inspiration, the source points at the community corpus linked from pretext.cool, including Pretext Breaker, Tetris × Pretext, a dragon animation, the Somnai editorial engine, Bad Apple!! ASCII, a drag-sprite reflow demo, and the Alarmy editorial clock, plus the official playground at chenglou.me/pretext.