SkillAgentSearch skills...

Dropflow

A CSS layout engine

Install / Use

/learn @chearon/Dropflow
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<img height=50 src=assets/logo.png>

Dropflow is a CSS layout engine created to explore the reaches of the foundational CSS standards (that is: inlines, blocks, floats, positioning and eventually tables, but not flexbox or grid). It has a high quality text layout implementation and is capable of displaying many of the languages of the world. You can use it to generate PDFs or images on the backend with Node and node-canvas or render rich, wrapped text to a canvas in the browser.

Features

  • Supports over 30 properties including complex ones like float
  • Bidirectional and RTL text
  • Hyperscript (h()) API with styles as objects in addition to accepting HTML and CSS
  • Any OpenType/TrueType buffer can (and must) be registered
  • JPEG, BMP, PNG, and GIF <img>s supported (backend support may differ)
  • Font fallbacks at the grapheme level
  • Colored diacritics
  • Desirable line breaking (e.g. carries starting padding to the next line)
  • Optimized shaping
  • Inherited and cascaded styles are never calculated twice
  • Handles as many CSS layout edge cases as I can find
  • Fully typed
  • Lots of tests
  • Fast

Supported CSS rules

Following are rules that work or will work soon. Shorthand properties are not listed. If you see all components of a shorthand (for example, border-style, border-width, border-color) then the shorthand is assumed to be supported (for example border).

Inline formatting

| Property | Values | Status | | -- | -- | -- | | <code>color</code> | rgba(), rgb(), #rrggbb, #rgb, #rgba | ✅‍ Works | | <code>direction</code> | ltr, rtl | ✅‍ Works | | <code>font-‍family</code> | | ✅‍ Works | | <code>font-‍size</code> | em, px, smaller etc, small etc, cm etc | ✅‍ Works | | <code>font-‍stretch</code> | condensed etc | ✅‍ Works | | <code>font-‍style</code> | normal, italic, oblique | ✅‍ Works | | <code>font-‍variant</code> | | 🚧‍ Planned | | <code>font-‍weight</code> | normal, bolder, lighter light, bold, 100-900 | ✅‍ Works | | <code>letter-‍spacing</code> | | 🚧‍ Planned | | <code>line-‍height</code> | normal, px, em, %, number | ✅‍ Works | | <code>tab-‍size</code> | | 🚧‍ Planned | | <code>text-‍align</code> | start, end, left, right, center | ✅‍ Works | | <code>text-‍align</code> | justify | ✅‍ Works | | <code>text-‍decoration</code> | | 🚧‍ Planned | | <code>unicode-‍bidi</code> | | 🚧‍ Planned | | <code>vertical-‍align</code> | baseline, middle, sub, super, text-top, text-bottom, %, px etc, top, bottom | ✅‍ Works | | <code>white-‍space</code> | normal, nowrap, pre, pre-wrap, pre-line | ✅‍ Works | | <code>word-‍break</code><br><code>overflow-‍wrap</code>,<code>word-‍wrap</code> | break-word, normal<br>anywhere, normal | ✅‍ Works | | <code>word-‍spacing</code> | normal, %, number | ✅‍ Works |

Block formatting

| Property | Values | Status | | -- | -- | -- | | <code>clear</code> | left, right, both, none | ✅‍ Works | | <code>float</code> | left, right, none | ✅‍ Works | | <code>writing-‍mode</code> | horizontal-tb, vertical-lr, vertical-rl | 🏗 Partially done<sup>1</sup> |

<sup>1</sup>Implemented for BFCs but not IFCs yet

Boxes and positioning

| Property | Values | Status | | -- | -- | -- | | <code>background-‍clip</code> | border-box, content-box, padding-box | ✅‍ Works | | <code>background-‍color</code> | rgba(), rgb(), #rrggbb, #rgb, #rgba | ✅‍ Works | | <code>border-‍color</code> | rgba(), rgb(), #rrggbb, #rgb, #rgba | ✅‍ Works | | <code>border-‍style</code> | solid, none | ✅‍ Works | | <code>border-‍width</code> | em, px, cm etc | ✅‍ Works | | <code>top</code>, <code>right</code>, <code>bottom</code>, <code>left</code> | em, px, %, cm etc | ✅‍ Works | | <code>box-‍sizing</code> | border-box, content-box | ✅‍ Works | | <code>display</code> | block | ✅‍ Works | | <code>display</code> | inline | ✅‍ Works | | <code>display</code> | inline-block | ✅‍ Works | | <code>display</code> | flow-root | ✅‍ Works | | <code>display</code> | none | ✅‍ Works | | <code>display</code> | table | 🚧‍ Planned | | | <code>height</code> | em, px, %, cm etc, auto | ✅‍ Works | | <code>margin</code> | em, px, %, cm etc, auto | ✅‍ Works | | <code>max-height</code>, <code>max-width</code>,<br><code>min-height</code>, <code>min-width</code> | em, px, %, cm etc, auto | 🚧‍ Planned | | <code>padding</code> | em, px, %, cm etc | ✅‍ Works | | <code>position</code> | absolute | 🚧‍ Planned | | <code>position</code> | fixed | 🚧‍ Planned | | <code>position</code> | relative | ✅‍ Works | | <code>transform</code> | | 🚧‍ Planned | | <code>overflow</code> | hidden, visible | ✅‍ Works | | <code>width</code> | em, px, %, cm etc, auto | ✅‍ Works | | <code>z-index</code> | number, auto | ✅‍ Works | | <code>zoom</code> | number, % | ✅‍ Works |

Usage

Dropflow works off of a DOM with inherited and calculated styles, the same way that browsers do. You create the DOM with the familiar h() function, and specify styles as plain objects.

import * as flow from 'dropflow';
import {createCanvas} from 'canvas';
import fs from 'node:fs';

// Register fonts before layout. This is a required step.
const roboto1 = new flow.FontFace('Roboto', new URL('file:///Roboto-Regular.ttf'), {weight: 400});
const roboto2 = new flow.FontFace('Roboto', new URL('file:///Roboto-Bold.ttf'), {weight: 700});
flow.fonts.add(roboto1).add(roboto2);

// Always create styles at the top-level of your module if you can.
const divStyle = flow.style({
  backgroundColor: {r: 28, g: 10, b: 0, a: 1},
  textAlign: 'center',
  color: {r: 179, g: 200, b: 144, a: 1}
});

// Since we're creating styles directly, colors are numbers
const spanStyle = flow.style({
  color: {r: 115, g: 169, b: 173, a: 1},
  fontWeight: 700
});

// Create a DOM
const el = flow.dom(
  flow.h('div', {style: divStyle}, [
    'Hello, ',
    flow.h('span', {style: spanStyle}, ['World!'])
  ])
);

// Layout and paint into the entire canvas (see also renderToCanvasContext)
const canvas = createCanvas(250, 50);
await flow.renderToCanvas(el, canvas);

// Save your image
fs.writeFileSync(new URL('file:///hello.png'), canvas.toBuffer());
<div align="center">

Hello world against a dark background, with "world" bolded and colored differently

</div>

HTML

This API is only recommended if performance is not a concern, or for learning purposes. Parsing adds extra time (though it is fast thanks to @fb55) and increases bundle size significantly.

import * as flow from 'dropflow';
import parse from 'dropflow/parse.js';
import {createCanvas} from 'canvas';
import fs from 'node:fs';

const roboto1 = new flow.FontFace('Roboto', new URL('file:///Roboto-Regular.ttf'), {weight: 400});
const roboto2 = new flow.FontFace('Roboto', new URL('file:///Roboto-Bold.ttf'), {weight: 700});
flow.fonts.add(roboto1).add(roboto2);

const el = parse(`
  <div style="background-color: #1c0a00; color: #b3c890; text-align: center;">
    Hello, <span style="color: #73a9ad; font-weight: bold;">World!</span>
  </div>
`);

const canvas = createCanvas(250, 50);
flow.renderToCanvas(el, canvas);

canvas.createPNGStream().pipe(fs.createWriteStream(new URL('hello.png', import.meta.url)));

Performance characteristics

Performance is a top goal and is second only to correctness. Run the performance examples in the examples directory to see the numbers for yourself.

  • 8 paragraphs with several inline spans of different fonts can be turned from HTML to image in 9ms on a 2019 MacBook Pro and 13ms on a 2012 MacBook Pro (perf-1.ts)
  • The Little Prince (over 500 paragraphs) can be turned from HTML to image in under 160ms on a 2019 MacBook Pro and under 250ms on a 2012 MacBook Pro (perf-2.ts)
  • A 10-letter word can be generated and laid out (not painted) in under 25µs on a 2019 MacBook Pro and under 50µs on a 2012 MacBook Pro (perf-3.ts)

The fastest performance can be achieved by using the hyperscript API, which creates a DOM directly and skips the typical HTML and CSS parsing steps. Take care to re-use style objects to get the most benefits. Reflows at different widths are faster than recreating the layout tree.

API

The first two steps are:

  1. Register fonts
  2. Create a DOM via the Hyperscript or Parse API

Then, you can either render the DOM into a canvas using its size as the viewport:

  1. Render DOM to canvas

Or, you can use the lower-level functions to retain the layout, in case you want to reflow at a different size, choose not to paint (for example if the layout isn't visible) or get intrinsics:

  1. Load dependent resources
  2. Create a layout for the DOM
  3. Reflow the layout
  4. Paint the layout to a target like HTML5 canvas

Fonts

The first step in a dropflow program is to register fonts to be selected by the CSS font properties. Dropflow does not search system fonts, so you must construct a FontFace and add it at least once. The font registration API implements a subset of the CSS Font Loading API and adds one non-standard method, loadSync.

file:/// URLs will load() synchronously

View on GitHub
GitHub Stars1.4k
CategoryDevelopment
Updated2d ago
Forks31

Languages

TypeScript

Security Score

90/100

Audited on Mar 30, 2026

No findings