A thousand rectangles thrown down without overlapping, then each one grown until it hits something.
Scatter, then grow
Two phases, and the second is where the composition comes from.
Scatter. A thousand attempts to place a small rectangle at a random position with
random half-widths. Any attempt that overlaps an existing rectangle is thrown away —
not retried, just dropped — so the number that survive is well under the number
attempted, and the survivors are guaranteed disjoint.
var rect = new Rect(x - xr, x + xr, y - yr, y + yr);
foreach (var other in rects)
if (rect.Intersects(other)) { intersects = true; break; }
if (!intersects) rects.Add(rect);
Grow. Each surviving rectangle is then expanded on all four sides until it meets
a neighbour or the border.
The seeds, before any growth. Small, sparse, and with the seed padding keeping them
away from the frame edge.
Order decides everything
The growth is a single pass in list order, and each rectangle grows against the
current positions of the others — which means against the already-grown shapes of
everything before it and the seed shapes of everything after it.
foreach (var rect in rects)
{
// for each of the four sides: find the nearest blocking edge, move out to it
}
So the first rectangle in the list expands into almost empty space and takes as much
as it wants. The last expands into a field that is already full and barely moves.
Nothing arbitrates; the order is the arbitration, and it is why the finished pack has
large plates and thin slivers rather than a uniform grid.
A sixth of the way through the pass. The rectangles that have grown are noticeably
larger than the ones that have not, and they were not larger to begin with.
A third. Watch what is happening to the gaps: a rectangle that grew early has already
closed off routes for its neighbours, and the shapes still to grow will end up
defined by its edges.
Reaching for the border
One clause makes the difference between a pack and a grid:
var hitBorder = random.NextDouble() < HitSidesChance;
…
if (hitBorder || newVal != border) rect.X1 = newVal;
By default a rectangle only expands towards a side if there is something on that side
to stop it. If nothing blocks it, it stays put rather than running to the frame edge.
The HitSidesChance clause — one rectangle in five — overrides that and lets it
stretch all the way to the border regardless.
That single bit per rectangle is what produces the long spans that reach the frame
and cross the whole composition, and without it the pack would have a soft, unresolved
perimeter.
Two thirds.
Complete. Every rectangle is then shrunk slightly so its edges separate from its
neighbours’ — the pack is drawn with visible gaps, though the growth phase produced
none.
The hatching
Most rectangles are filled with parallel hatch lines rather than left empty:
HatchChance decides whether a rectangle is hatched at all, and SparseHatchChance
whether it takes the wide spacing instead of the tight one. So a rectangle carries
one of three tones — empty, sparse, dense — chosen independently of its size.
Because the choice ignores size, a large plate is as likely to be empty as a sliver,
and the tonal composition is unrelated to the geometric one. The two structures are
laid over each other rather than derived from each other.
One variant adds a fourth tone. FillChance paints a share of the cells solid
before the outline is drawn, and a filled cell skips its hatch — so the pack
carries flat panels between the hatched and the empty.
Seven packs
| Rendering | What changes | Ink |
|---|
| Ice Blue | — | ice blue on black |
| Coral Glow | larger seeds: a coarse pack of big plates, airy hatching | coral on near-black |
| Neon Lime | border rolls on half the rectangles, most cells left empty: long spans | lime on near-black |
| Blueprint | fine seeds and a fine stroke on a pale ground: the palette inverts | blueprint blue on paper |
| Gilded | a share of the cells filled solid | gold on warm black |
| Violet Rain | every seed stretched tall, mostly empty outlines | violet on plum black |
| Stardust | no outlines, a centre dot per cell | pale starlight on deep blue |
Each rendering has its own seed and its own parameter changes, so the seven are
independent packs rather than one arrangement recoloured. The shared core is the
pipeline: scatter, grow, shrink, then frame and hatch.
Fitted to the canvas
The pack’s envelope is not the fixed ±100 square the seeds scatter in. Which
seeds survive and how far each grows is the RNG’s doing, so a pack can crowd
one edge of its square and fall short of another. Drawing that square at a
fixed scale would make every preset lopsided in its own way.
So the drawing measures the pack it has built and fits that envelope to the
canvas instead: centred, and sized so the wider axis runs out to a fixed
margin (FitMargin, 7% of the canvas side). Every preset hangs in the same
deliberate frame, whatever shape its pack grew into. The progression’s stages
are drawn at the finished pack’s scale, so the frame never moves while the
pack grows into it.
On the wall
The gallery closes with the piece hung. MuseumFrame — a presentation layer
that composes a finished piece without touching its generation — renders the
lead at the plate’s own size and hangs it on a dark wall with a soft shadow,
two frame hairlines, and a small wall label. One plate, not a framed copy of
every variant: the frame is how the piece is presented, not what it generates.
Determinism
SeededRandom — the project’s stable Xoshiro256StarStar — is seeded from the
preset and consumed in three fixed phases: seed placement, one draw per rectangle for
the border clause, and the fill and hatching choices at draw time. The order is
fixed, so the same seed produces the same pack. The canvas fit builds the pack a
second time from the same seed and takes the same rolls — the measurement is the
drawing’s twin, generated but never drawn.