A word reserves an axis-aligned box, and a long word's box is wide and short. Turn that word
a quarter turn and the box becomes narrow and tall — which is exactly the shape of the gaps
an upright-only cloud leaves behind. The rotate option lets the search use both
orientations, so those gaps get filled.
mode picks the packing strategy
(pretty / balanced / fast); rotate picks
the orientations that strategy may use. Same spiral, same collision bitmap, same
ordering — so you can mix orientations without giving up placement quality.
rotate | Orientations | Resulting look |
|---|---|---|
"none"default |
Every word horizontal — rotation is always 0. |
The clouds this library has always produced. Easiest to read; leaves tall, narrow gaps unused. |
"mixed90" |
A mix of rotation: 0 and rotation: 90. |
A denser, more textured cloud. More words fit, because a long word can drop into a slot no upright word would fit in. |
rotate alongside mode
One extra option, and the placements you get back have the same shape as always — the only
difference is that rotation is now sometimes 90.
import { layout } from "./js/index.js"; const placements = await layout(items, { mode: "pretty", // packing strategy — unchanged rotate: "mixed90", // let words turn a quarter turn where it fits width: 960, height: 600, });
Omit rotate and you get "none". Anything other than those two
names throws a RangeError.
x/y when drawing
x/y is the center of the rotated bounding box, so
translating there and rotating puts the word exactly where the engine reserved space for
it. One caveat: baselineOffset measures straight down from an
upright box's center, and for a turned word that is no longer "down" on
screen. Use textBaseline: "middle" for rotated words and keep the exact
alphabetic baseline for upright ones.
ctx.textAlign = "center"; for (const p of placements) { ctx.save(); ctx.translate(p.x, p.y); // center of the rotated box ctx.font = `${p.fontSize}px system-ui, sans-serif`; if (p.rotation) { ctx.rotate((p.rotation * Math.PI) / 180); ctx.textBaseline = "middle"; // baselineOffset is for upright text ctx.fillText(p.text, 0, 0); } else { ctx.textBaseline = "alphabetic"; ctx.fillText(p.text, 0, p.baselineOffset); } ctx.restore(); }
A renderer that already did if (p.rotation) ctx.rotate(...) works unchanged —
the baseline split above is a refinement, not a requirement.
"mixed90" to be slower than
"none".fast ignores it. Shelf packing stacks rows of uniform
height, which only reads as a block when every word runs the same direction.The same words, the same mode, the same seed — laid out with each orientation policy so you can compare the upright cloud against the mixed one.
Compare the "placed" counts: the mixed cloud usually fits more of the same words on the same canvas, and the difference widens as the word count grows. Watch the gaps too — the tall slivers between big upright words in the left cloud are where the right one drops a long word on its side. The timing readout shows the other half of the trade.