https://github.com/chearon/dropflow
Skip to content
Toggle navigation
Sign in
* Product
+
Actions
Automate any workflow
+
Packages
Host and manage packages
+
Security
Find and fix vulnerabilities
+
Codespaces
Instant dev environments
+
Copilot
Write better code with AI
+
Code review
Manage code changes
+
Issues
Plan and track work
+
Discussions
Collaborate outside of code
Explore
+ All features
+ Documentation
+ GitHub Skills
+ Blog
* Solutions
For
+ Enterprise
+ Teams
+ Startups
+ Education
By Solution
+ CI/CD & Automation
+ DevOps
+ DevSecOps
Resources
+ Learning Pathways
+ White papers, Ebooks, Webinars
+ Customer Stories
+ Partners
* Open Source
+
GitHub Sponsors
Fund open source developers
+
The ReadME Project
GitHub community articles
Repositories
+ Topics
+ Trending
+ Collections
* Pricing
Search or jump to...
Search code, repositories, users, issues, pull requests...
Search
[ ]
Clear
Search syntax tips
Provide feedback
We read every piece of feedback, and take your input very seriously.
[ ] [ ] Include my email address so I can be
contacted
Cancel Submit feedback
Saved searches
Use saved searches to filter your results more quickly
Name [ ]
Query [ ]
To see all available qualifiers, see our documentation.
Cancel Create saved search
Sign in
Sign up
You signed in with another tab or window. Reload to refresh your
session. You signed out in another tab or window. Reload to refresh
your session. You switched accounts on another tab or window. Reload
to refresh your session. Dismiss alert
{{ message }}
chearon / dropflow Public
* Notifications
* Fork 5
* Star 387
*
A CSS layout engine
chearon.github.io/dropflow/
387 stars 5 forks Branches Tags Activity
Star
Notifications
* Code
* Issues 4
* Pull requests 0
* Actions
* Projects 0
* Security
* Insights
Additional navigation options
* Code
* Issues
* Pull requests
* Actions
* Projects
* Security
* Insights
chearon/dropflow
This commit does not belong to any branch on this repository, and may
belong to a fork outside of the repository.
master
BranchesTags
Go to file
Code
Folders and files
Name Name Last commit Last commit
message date
Latest commit
History
510 Commits
.github/workflows .github/
workflows
assets assets
dist dist
examples examples
gen gen
site site
src src
test test
.gitignore .gitignore
README.md README.md
build.sh build.sh
bun.lockb bun.lockb
gen.js gen.js
package.json package.json
tsconfig.json tsconfig.json
View all files
Repository files navigation
* README
dropflow
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
* 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
color rgba(), rgb(), #rrggbb, #rgb, #rgba Works
direction ltr, rtl Works
font-family Works
font-size em, px, smaller etc, small etc, cm etc Works
font-stretch condensed etc Works
font-style normal, italic, oblique Works
font-variant Planned
font-weight normal, bolder, lighter light, bold, Works
100-900
line-height normal, px, em, %, number Works
tab-size Planned
text-align start, end, left, right, center Works
text-decoration Planned
unicode-bidi Planned
vertical-align baseline, middle, sub, super, text-top, Works
text-bottom, %, px etc, top, bottom
white-space normal, nowrap, pre, pre-wrap, pre-line Works
Block formatting
Property Values Status
clear left, right, both, none Works
float left, right, none Works
writing-mode horizontal-tb, vertical-lr, Partially done^
vertical-rl 1
^1Implemented for BFCs but not IFCs yet
Boxes and positioning
Property Values Status
background-clip border-box, content-box, Works
padding-box
background-color rgba(), rgb(), #rrggbb, #rgb, # Works
rgba
border-color rgba(), rgb(), #rrggbb, #rgb, # Works
rgba
border-style solid, none Works
border-width em, px, cm etc Works
top, right, bottom, em, px, %, cm etc Works
left
box-sizing border-box, content-box Works
display block Works
display inline Works
display inline-block Works
display flow-root Works
display none Works
display table Planned
height em, px, %, cm etc, auto Works
margin em, px, %, cm etc, auto Works
max-height, max-width, em, px, %, cm etc, auto Planned
min-height, min-width
padding em, px, %, cm etc Works
position absolute Planned
position fixed Planned
position relative Works
transform Planned
overflow Planned
width em, px, %, cm etc, auto Works
z-index number, auto 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.
// It is only async when you don't pass an ArrayBuffer
await flow.registerFont(new URL('fonts/Roboto-Regular.ttf', import.meta.url));
await flow.registerFont(new URL('fonts/Roboto-Bold.ttf', import.meta.url));
// Always create styles at the top-level of your module if you can
const divStyle = {
backgroundColor: {r: 28, g: 10, b: 0, a: 1},
color: {r: 179, g: 200, b: 144, a: 1},
textAlign: 'center'
};
// Since we're creating styles directly, colors have to be defined numerically
const spanStyle = {
color: {r: 115, g: 169, b: 173, a: 1},
fontWeight: 700
};
// Create a DOM
const rootElement = 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);
flow.renderToCanvas(rootElement, canvas, /* optional density: */ 2);
// Save your image
canvas.createPNGStream().pipe(fs.createWriteStream(new URL('hello.png', import.meta.url)));
Hello world against a dark background, with "world" bolded and
colored differently
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/with-parse.js';
import {createCanvas} from 'canvas';
import fs from 'node:fs';
await flow.registerFont(new URL('fonts/Roboto-Regular.ttf', import.meta.url));
await flow.registerFont(new URL('fonts/Roboto-Bold.ttf', import.meta.url));
const rootElement = flow.parse(`
Hello, World!
`);
const canvas = createCanvas(250, 50);
flow.renderToCanvas(rootElement, canvas, 2);
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 25us on a 2019 MacBook Pro and under 50us 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 re-layout at a different size, choose not to paint
(for example if the layout isn't visible) or get intrinsics:
1. Generate a tree of layout boxes from the DOM
2. Layout the box tree
3. Paint the box tree to a target like canvas
Fonts
registerFont
async function registerFont(url: URL, options?: {paint: boolean}): Promise;
async function registerFont(buffer: ArrayBuffer, url: URL, options?: {paint: boolean}): Promise;
Registers a font to be selected by the font properties. Dropflow does
not search system fonts, so you must do this with at least one font.
When a URL is passed, don't forget to await this. If an ArrayBuffer
is passed, there is no need to await. In that function signature, the
URL is only used to provide a unique name for the font.
The URL must always be unique.
In the browser, make sure the font is also loaded into page so that
the paint backend can reference it with ctx.font. In node-canvas, you
should either use registerFont from canvas for this font, or pass
{paint: true} for options, which will try to load node-canvas and
call its registerFont.
Note
This will soon be replaced with an API that looks more like the
document.fonts API in the browser.
unregisterFont
function unregisterFont(url: URL): void;
Removes a font from the internal list so that it won't be picked by
the font properties. This does not remove it from the paint target.
Hyperscript
The hyperscript API is the fastest way to generate a DOM.
h
type HsChild = HTMLElement | TextNode | string;
interface HsData {
style?: DeclaredPlainStyle;
attrs?: {[k: string]: string};
}
function h(tagName: string): HTMLElement;
function h(tagName: string, data: HsData): HTMLElement;
function h(tagName: string, children: HsChild[]): HTMLElement;
function h(tagName: string, text: string): HTMLElement;
function h(tagName: string, data: HsData, children: HsChild[] | string): HTMLElement;
Creates an HTMLElement. Styles go on data.style (see style.ts for
supported values and their types).
dom
function dom(el: HTMLElement | HTMLElement[]): HTMLElement
Calculates styles and wraps with if the root tagName is not
"html".
The entire h tree to render must be passed to this function before
rendering.
Parse
This part of the API brings in a lot more code due to the size of the
HTML and CSS parsers. Import it like so:
import flow from 'dropflow/with-parse.js';
Note that only the style HTML attribute is supported at this time.
class does not work yet.
parse
function parse(str: string): HTMLElement;
Parses HTML. If you don't specify a root element, content will
be wrapped with one.
Render DOM to canvas
This is only for simple use cases. For more advanced usage continue
on to the next section.
function renderToCanvas(rootElement: HTMLElement, canvas: Canvas): void;
Renders the whole layout to the canvas, using its width and height as
the viewport size.
Generate
generate
function generate(rootElement: HTMLElement): BlockContainer
Generates a box tree for the element tree. Box trees roughly
correspond to DOM trees, but usually have more boxes (like for
anonymous text content between block-level elements (divs)) and
sometimes fewer (like for display: none).
BlockContainer has a repr() method for logging the tree.
Hold on to the return value so you can lay it out many times in
different sizes, paint it or don't paint it if it's off-screen, or
get intrinsics to build a higher-level logical layout (for example,
spreadsheet column or row size even if the content is off screen).
Layout
layout
function layout(root: BlockContainer, width = 640, height = 480);
Position boxes and split text into lines so the layout tree is ready
to paint. Can be called over and over with a different viewport size.
In more detail, layout involves:
* Margin collapsing for block boxes
* Passing text to HarfBuzz, iterating font fallbacks, wrapping,
reshaping depending on break points
* Float placement and clearing
* Positioning shaped text spans and backgrounds according to
direction and text direction
* Second and third pass layouts for intrinsics of float,
inline-block, and absolutes
* Post-layout positioning (position)
Paint
This step paints the layout to a target. Painting can be done as many
times as needed (for example, every time you clear and render all of
your scene to the canvas).
Canvas is currently the only seriously supported target, but other
targets will be added, like pdf.js and SVG. There is also a toy HTML
target that was used early on in development, and kept around for
fun.
paintToCanvas
function paintToCanvas(root: BlockContainer, ctx: CanvasRenderingContext2D): void;
Paints the layout to a browser canvas, node-canvas, or similar
standards-compliant context.
paintToHtml
function paintToHtml(root: BlockContainer): string;
Paint to HTML! Yes, this API can actually be used to go from HTML to
HTML. It generates a flat list of a bunch of absolutely positioned
elements. Probably don't use this, but it can be useful in
development and is amusing.
Other
staticLayoutContribution
function staticLayoutContribution(box: BlockContainer): number;
Returns the inline size in CSS pixels taken up by the layout, not
including empty space after lines or the effect of any width
properties. layout must be called before this.
The intended usage is this: after laying out text into a desired
size, use staticLayoutContribution to get the size without any
remaining empty space at the end of the lines, then layout again into
that size to get a tightly fitting layout.
HarfBuzz
Glyph layout is performed by HarfBuzz compiled to WebAssembly. This
allows for a level of correctness that isn't possible by using the
measureText API to position spans of text. If you color the "V" in
the text "AV" differently in Google Sheets, you will notice kerning
is lost, and the letters appear further apart than they should be.
That's because two measureText and fillText calls were made on the
letters, so contextual glyph advances were lost. Dropflow uses
HarfBuzz on more coarse shaping boundaries (not when color is
changed) so that the font is more correctly supported.
HarfBuzz compiled to WebAssembly can achieve performance metrics
similar to CanvasRenderingContext2D's measureText. It's not as fast
as measureText, but it's not significantly slower (neither of them
are the dominators in a text layout stack) and measureText has other
correctness drawbacks. For example, a measureText-based text layout
implementation must use a word cache to be quick, and this is what
GSuite apps do. But a word cache is not able to support fonts with
effects across spaces, and to support such a font would have to
involve a binary search on the paragraph's break indices, which is
far slower than passing the whole paragraph to HarfBuzz. Colored
diacritics are not possible in any way with measureText either.
Shout-outs
dropflow doesn't have any package.json dependencies, but the work of
many others made it possible. Javascript dependencies have been
checked in and modified to varying degrees to fit this project,
maintain focus, and rebel against dependency-of-dependency madness.
Here are the projects I'm grateful for:
* harfbuzz does font shaping and provides essential font APIs (C++)
* Tehreer/SheenBidi calculates bidi boundaries (C++)
* foliojs/linebreak provides Unicode break indices (JS, modified)
* foliojs/grapheme-breaker provides Unicode grapheme boundaries
(JS, modified)
* peggyjs/peggy builds the CSS parser (JS, dev dependency)
* fb55/htmlparser2 parses HTML (JS, modified)
* google/emoji-segmenter segments emoji (C++)
* foliojs/unicode-trie is used for fast unicode data (JS, heavily
modified to remove unused parts)
About
A CSS layout engine
chearon.github.io/dropflow/
Resources
Readme
Activity
Stars
387 stars
Watchers
4 watching
Forks
5 forks
Report repository
Releases
No releases published
Languages
* TypeScript 40.9%
* C++ 39.2%
* JavaScript 16.6%
* PEG.js 2.1%
* C 0.5%
* Shell 0.3%
* Other 0.4%
Footer
(c) 2024 GitHub, Inc.
Footer navigation
* Terms
* Privacy
* Security
* Status
* Docs
* Contact
* Manage cookies
* Do not share my personal information
You can't perform that action at this time.