Skip to content
glyphcss

Fluid

Move the pointer over it to stir the fluid; tune the field and the reader in the fluid workbench.

@glyphcss/fluid is a living braille fluid as a glyphcss surface. A shallow-water simulation runs on a cell grid; DOM text, images and meshes become targets the ink flows toward and condenses into, then melts back out of. Each frame is one textContent write to one <pre>, drawn as 8-dot braille with per-cell colour through glyphcss’s colour-font atlas. The simulation runs in a Web Worker.

import { createGlyphFluid, GlyphFluidTargets } from "@glyphcss/fluid";
const host = document.querySelector<HTMLElement>("#fluid")!;
const fluid = createGlyphFluid(host);
await fluid.ready;
const h1 = document.querySelector<HTMLElement>("h1")!;
h1.style.color = "transparent"; // the fluid draws it; the element keeps layout and accessibility
const title = fluid.addTarget(GlyphFluidTargets.text(h1, { color: "#3ccf96" }));
title.melt(); // flows out; title.condense() flows back in
// On unmount: fluid.destroy();

Install with npm install @glyphcss/fluid glyphcss. There are no React/Vue bindings: create the fluid in your framework’s mount hook and destroy() it on unmount.

Each layer works alone; only the last touches the DOM.

GlyphFluidSim → computeGlyphFluidCells → GlyphFluidEncoder → createGlyphFluid
(the fluid sim, (braille mask + colour (glyphcss atlas text (targets, worker,
bit-identical) per cell) + pooled palette) one <pre>)
  • GlyphFluidSim steps bit-identically to a reference implementation of the step.
  • computeGlyphFluidCells is the renderer’s per-cell decision as data: which of the 8 dots light and in what colour.
  • GlyphFluidEncoder turns cells into atlas text against a pooled palette, so the palette CSS changes only when it repools.
  • createGlyphFluid composes targets and zones, runs the pipeline in a worker and writes the frame.
  • @glyphcss/fluid/reader adds a speed reader on top.
TaskMethods
ContentaddTarget(spec) → { enabled, condense, melt, remove }; addZone({ el, kind }) → { remove }
Motionimpulse(clientX, clientY, { strength, radius }), push(clientX, clientY, dx, dy)
Clockstep(dt) with clock: "manual"; relayout() after layout changes the observers miss
Lifecycleready, stats(), destroy()

stats() reports fps, pipeline ("worker" or "main"), encoding, cols, rows, frames, stalls, stepMs and lit cells.