A hundred towers of stacked ellipses, drawn height by height so the whole field rises at once.
Height is the outer loop
The obvious way to draw a field of towers is one tower at a time. This piece does
not:
for (float height = 0f; height < ceiling; height += Interspacing)
foreach (var tower in towers)
{
if (tower.Height <= height) continue;
using var ring = tower.RingPath(height, …);
canvas.DrawPath(ring, fill);
canvas.DrawPath(ring, stroke);
}
Height is the outer loop and the tower list the inner one, so every tower gains one
ring before any tower gains its second. The scene grows as a single surface rising
out of the ground, and the progression below is that literal order of drawing.
The reason it has to be that way is occlusion. Each ring is filled with the
background colour before it is stroked, so a ring painted later hides whatever it
covers. Drawing bottom to top, and back to front, is what turns a stack of outlines
into something that reads as solid.
The first few rings. Every one of the hundred towers is already placed — what is
missing is height, not population.
Placing the towers
The layout is a greedy dart-throwing pack. For each new tower the code throws ten
candidate points, discards any that fall inside an existing tower, and keeps the one
that ended up furthest from its nearest neighbour:
float gap = SKPoint.Distance(candidate, t.Position) - (t.Radius + RMin);
if (gap < 0) { overlaps = true; break; }
if (gap < nearestGap) nearestGap = gap;
Then — and this is the part that makes the field look considered — the winning gap
becomes the new tower’s radius, clamped between RMin and RMax. A tower placed
in open ground is fat; one squeezed into a corner is thin. The packing decides the
sizes, so the towers fit their space by construction rather than by luck.
After placing, the list is sorted by Position.Y descending, which is the painter’s
order: towers further back are drawn first, and every later ring can only ever hide
something behind it.
The short towers have finished. tower.Height <= height retires each one as the
ceiling passes it, so the field thins as it rises.
A tower’s base is a plain circle of points. Every ring it draws is that circle put
through three changes:
Taper. Conify scales the ring linearly with height. Positive shrinks the
ring toward the top, so a tower is a cone rather than a cylinder — at 0.5
the top is half the width of the base. Zero keeps every ring the same size, and
negative runs the cone backwards: a point at the foot flaring as it climbs,
which turns each tower into a goblet.
Perspective. The y coordinate is divided by Perspective, squashing each circle
into an ellipse. That single division is the entire three-dimensional illusion —
there is no camera, no projection matrix, no depth buffer.
Jitter. Each ring is offset by a fresh random dx, dy of up to
Noise × Radius / 3.5. Small, but applied per ring rather than per tower, so the
stacks wander slightly as they climb and no tower is perfectly straight.
Halfway. The taper and the jitter are both legible now, and the tallest towers have
begun to overlap the ones behind them.
Most of the way. Only the tall towers are still growing, and the field has opened
up.
Full height. The frame is a real clip, not a decoration — the drawing is clipped to
(100 − Border) × 4 units square and the border is stroked on the same rectangle, so
towers are cut off cleanly at the edge rather than being kept inside it.
Five fields
Five renderings ship, and each is a different place in the parameter space
rather than a recolouring of the same drawing:
| Rendering | Towers | Conify | Perspective | Radius | Ink |
|---|
| Cyan Towers | 100 | 0.5 | 1.4 | 5–30 | #00FFFF on black |
| Amber Chalices | 85 | −1.0 | 1.5 | 7–24 | #FFB84D on #140D04 |
| Emerald Monoliths | 26 | 0.25 | 1.5 | 10–30 | #4CE68B on #04120A |
| Ivory Wells | 85 | 0 | 2.6 | 8–22 | #F4F6F2 on #0A0E16 |
| Paper Towers | 85 | 0.55 | 1.45 | 7–26 | #26221A on #F4F1E8 |
Cyan Towers is the lead: a dense field of tapered spires, the classic form of
the piece. Amber Chalices runs the taper backwards, so the towers flare from a
point at the foot into full-width rims — goblets instead of spires. Emerald
Monoliths pushes to the other end of the packing: twenty-six towers with
deliberately quiet jitter, so scale and open ground do the composing. Ivory
Wells holds the taper at zero and flattens the camera, and each tower becomes
a stack of identical ellipses — concentric wells seen from above. Paper Towers
flips the treatment: charcoal ink on a warm paper ground, drawn full-bleed
with no frame, a print instead of a plate.
The tower count and the maximum radius pull against each other through the
packing: more towers means less open ground, which means the greedy gap is
smaller, which means thinner towers even before RMax clamps them. The five
fields sit on different points of that curve, and on different settings of the
three deformations.
The museum treatment
One plate is not a rendering but a presentation: the cyan towers hung as a
finished piece — on a gallery wall, behind a mat, in a dark frame. The wall,
the mat, and the frame are drawn by MuseumFrame, from SkiaSharp.Extensions,
wrapping the lead preset without touching it; the opening is sized to the
cyan render’s own pixels, so the towers inside sit unscaled on the mat.
It is one plate, not a family: the museum treatment frames the lead preset
only, because a framed copy of every variant would be a wall of duplicates
rather than a presentation.
Determinism
One Random is created per draw, seeded from the preset, and used for three things
in a fixed order: candidate positions, each tower’s radius and height, and then the
per-ring jitter. Because the jitter is consumed inside the height loop, the sequence
depends on the drawing order as well as on the seed — which is why the progression
draws layers in the same order the finished piece does, rather than tower by tower.
The generator is System.Random, which the project’s determinism policy flags as not
guaranteed stable across .NET versions.