Upright or mixed: turning words sideways

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.

Not a mode. 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.
rotateOrientationsResulting 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.

Using it

Step 1

Pass 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.

layout.js
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.

Step 2

Rotate around 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.

render.js
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.

Step 3

Know what it costs you

  • Still deterministic. The preferred orientation alternates down the weight-sorted list — upright for even positions, sideways for odd — rather than being drawn at random. Same words and options always give the same cloud.
  • Two masks per word. Each word is rasterized both ways up front (on the parallel path), and a word whose preferred orientation finds no gap pays a second spiral walk. Expect "mixed90" to be slower than "none".
  • The heaviest word still lands dead-center — it is position 0, so it stays upright.
  • fast ignores it. Shelf packing stacks rows of uniform height, which only reads as a block when every word runs the same direction.
Readability is the trade. Sideways words make a cloud denser and more decorative, but a reader has to tilt their head. For dashboards and anything scanned quickly, upright is usually the better default.

See the difference

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.

rotate: "none" — default

Press Esc or click Exit fullscreen to return to the demo card.

rotate: "mixed90"

Press Esc or click Exit fullscreen to return to the demo card.

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.