Procedural Grids: Square, Triangle, and Hexagon Patterns in Pathogen
Grid patterns show up everywhere — engineering overlays, graph paper, game boards, architectural plans, generative art backgrounds. Building one from scratch means nested loops, coordinate math, and careful edge deduplication for triangles and hexagons. Pathogen's three new grid functions collapse all of that into a single call.
squareGrid, triangleGrid, and hexagonGrid each generate complete path geometry within a bounding rectangle. A GridPatternType enum selects the visual style — Shape, Dot, Intersection, or Partial — and all three return PathSegment values, so they compose with layers, transforms, and clip paths just like circle() or polygon().
Square Grids
squareGrid(type, x, y, width, height, cellSize)
| Parameter | Description |
|---|---|
type |
GridPatternType — .Shape, .Dot, .Intersection, or .Partial |
x, y |
Top-left origin |
width, height |
Bounding dimensions |
cellSize |
Side length of each square cell |
The grid fills as many complete cells as fit: floor(width / cellSize) columns and floor(height / cellSize) rows. Extra space is ignored.
// viewBox="0 0 400 400"
// Square grid — all four pattern types
let gridColor = Color(CSSVar('--grid-color', #4488ff));
let bgColor = Color(CSSVar('--bg-color', #0f172a));
let cellSize = 14;
let gridW = 160;
let gridH = 150;
let pad = 20;
// Background
let bg = PathLayer('bg') ${ fill: bgColor; stroke: none; };
bg.apply { rect(0, 0, 400, 400) }
// Grid layers — each pattern in a different hue
let shapeColor = gridColor;
let dotColor = gridColor.hueShift(90);
let intColor = gridColor.hueShift(180);
let partColor = gridColor.hueShift(270);
let shapeLayer = PathLayer('shape') ${ stroke: shapeColor; stroke-width: 0.3; fill: none; };
let dotLayer = PathLayer('dot') ${ stroke: dotColor; stroke-width: 0.3; fill: dotColor; };
let intLayer = PathLayer('int') ${ stroke: intColor; stroke-width: 0.4; fill: none; stroke-linecap: round; };
let partLayer = PathLayer('part') ${ stroke: partColor; stroke-width: 0.4; fill: none; stroke-linecap: round; };
// Top-left: Shape
shapeLayer.apply {
squareGrid(GridPatternType.Shape, pad, pad + 15, gridW, gridH, cellSize);
}
// Top-right: Dot
dotLayer.apply {
squareGrid(GridPatternType.Dot, 220, pad + 15, gridW, gridH, cellSize);
}
// Bottom-left: Intersection
intLayer.apply {
squareGrid(GridPatternType.Intersection, pad, 215 + 15, gridW, gridH, cellSize);
}
// Bottom-right: Partial
partLayer.apply {
squareGrid(GridPatternType.Partial, 220, 220 + 15, gridW, gridH, cellSize);
}
// Labels
let labels = TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 11;
fill: #94a3b8;
text-anchor: start;
};
labels.apply {
text(pad, pad + 8)`Shape`
text(220, pad + 8)`Dot`
text(pad, 220 + 8)`Intersection`
text(220, 220 + 8)`Partial`
}
Triangle Grids
triangleGrid(type, x, y, width, height, cellSize)
Same parameters as squareGrid, but cellSize is the triangle height (altitude of the equilateral triangle). The side length is derived: side = 2 × cellSize / √3. Triangles alternate between point-up and point-down orientations, with odd rows offset by half a side length to form a seamless tessellation.
The intersection marks on triangle grids are edge-aligned — six arms at 60° intervals (three bidirectional lines), matching the grid's natural symmetry where six edges meet at each interior vertex.
// viewBox="0 0 400 400"
// Triangle grid — all four pattern types
let gridColor = Color(CSSVar('--grid-color', #44cc88));
let bgColor = Color(CSSVar('--bg-color', #0f172a));
let cellSize = 14;
let gridW = 160;
let gridH = 150;
let pad = 20;
// Background
let bg = PathLayer('bg') ${ fill: bgColor; stroke: none; };
bg.apply { rect(0, 0, 400, 400) }
// Grid layers
let shapeColor = gridColor;
let dotColor = gridColor.hueShift(90);
let intColor = gridColor.hueShift(180);
let partColor = gridColor.hueShift(270);
let shapeLayer = PathLayer('shape') ${ stroke: shapeColor; stroke-width: 0.3; fill: none; };
let dotLayer = PathLayer('dot') ${ stroke: dotColor; stroke-width: 0.3; fill: dotColor; };
let intLayer = PathLayer('int') ${ stroke: intColor; stroke-width: 0.4; fill: none; stroke-linecap: round; };
let partLayer = PathLayer('part') ${ stroke: partColor; stroke-width: 0.4; fill: none; stroke-linecap: round; };
// Top-left: Shape
shapeLayer.apply {
triangleGrid(GridPatternType.Shape, pad, pad + 15, gridW, gridH, cellSize);
}
// Top-right: Dot
dotLayer.apply {
triangleGrid(GridPatternType.Dot, 220, pad + 15, gridW, gridH, cellSize);
}
// Bottom-left: Intersection
intLayer.apply {
triangleGrid(GridPatternType.Intersection, pad, 215 + 15, gridW, gridH, cellSize);
}
// Bottom-right: Partial
partLayer.apply {
triangleGrid(GridPatternType.Partial, 220, 220 + 15, gridW, gridH, cellSize);
}
// Labels
let labels = TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 11;
fill: #94a3b8;
text-anchor: start;
};
labels.apply {
text(pad, pad + 8)`Shape`
text(220, pad + 8)`Dot`
text(pad, 220 + 8)`Intersection`
text(220, 220 + 8)`Partial`
}
Hexagon Grids
hexagonGrid(type, x, y, width, height, cellSize, orientation?)
The hexagon function adds a seventh parameter: orientation. It accepts the HexagonOrientation enum — .Edge for flat-top hexagons (an edge faces up) or .Vertex for pointy-top (a vertex faces up). When omitted, it defaults to .Edge.
| Orientation | Top | cellSize meaning |
|---|---|---|
HexagonOrientation.Edge |
Flat edge | Flat-to-flat height |
HexagonOrientation.Vertex |
Pointed vertex | Point-to-point height |
Intersection marks on hex grids are 3-arm radial marks — matching the three edges that meet at each vertex.
// viewBox="0 0 400 400"
// Hexagon grid — all four pattern types (flat-top)
let gridColor = Color(CSSVar('--grid-color', #ff6688));
let bgColor = Color(CSSVar('--bg-color', #0f172a));
let cellSize = 18;
let gridW = 160;
let gridH = 150;
let pad = 20;
// Background
let bg = PathLayer('bg') ${ fill: bgColor; stroke: none; };
bg.apply { rect(0, 0, 400, 400) }
// Grid layers
let shapeColor = gridColor;
let dotColor = gridColor.hueShift(90);
let intColor = gridColor.hueShift(180);
let partColor = gridColor.hueShift(270);
let shapeLayer = PathLayer('shape') ${ stroke: shapeColor; stroke-width: 0.3; fill: none; };
let dotLayer = PathLayer('dot') ${ stroke: dotColor; stroke-width: 0.3; fill: dotColor; };
let intLayer = PathLayer('int') ${ stroke: intColor; stroke-width: 0.4; fill: none; stroke-linecap: round; };
let partLayer = PathLayer('part') ${ stroke: partColor; stroke-width: 0.4; fill: none; stroke-linecap: round; };
// Top-left: Shape
shapeLayer.apply {
hexagonGrid(GridPatternType.Shape, pad, pad + 15, gridW, gridH, cellSize);
}
// Top-right: Dot
dotLayer.apply {
hexagonGrid(GridPatternType.Dot, 220, pad + 15, gridW, gridH, cellSize);
}
// Bottom-left: Intersection
intLayer.apply {
hexagonGrid(GridPatternType.Intersection, pad, 215 + 15, gridW, gridH, cellSize);
}
// Bottom-right: Partial
partLayer.apply {
hexagonGrid(GridPatternType.Partial, 220, 220 + 15, gridW, gridH, cellSize);
}
// Labels
let labels = TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 11;
fill: #94a3b8;
text-anchor: start;
};
labels.apply {
text(pad, pad + 8)`Shape`
text(220, pad + 8)`Dot`
text(pad, 220 + 8)`Intersection`
text(220, 220 + 8)`Partial`
}
Orientation Comparison
The two orientations produce visually distinct tessellations from the same cell size:
// viewBox="0 0 400 200"
// Hexagon orientations — flat-top (Edge) vs pointy-top (Vertex)
let gridColor = Color(CSSVar('--grid-color', #cc88ff));
let bgColor = Color(CSSVar('--bg-color', #0f172a));
let cellSize = 18;
let gridW = 150;
let gridH = 140;
let pad = 20;
// Background
let bg = PathLayer('bg') ${ fill: bgColor; stroke: none; };
bg.apply { rect(0, 0, 400, 200) }
// Grid layers
let flatColor = gridColor;
let pointyColor = gridColor.hueShift(120);
let flatLayer = PathLayer('flat') ${ stroke: flatColor; stroke-width: 0.4; fill: none; };
let pointyLayer = PathLayer('pointy') ${ stroke: pointyColor; stroke-width: 0.4; fill: none; };
// Left: Flat-top (Edge)
flatLayer.apply {
hexagonGrid(GridPatternType.Shape, pad, pad + 15, gridW, gridH, cellSize, HexagonOrientation.Edge);
}
// Right: Pointy-top (Vertex)
pointyLayer.apply {
hexagonGrid(GridPatternType.Shape, 220, pad + 15, gridW, gridH, cellSize, HexagonOrientation.Vertex);
}
// Labels
let labels = TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 11;
fill: #94a3b8;
text-anchor: start;
};
labels.apply {
text(pad, pad + 8)`Flat-top (Edge)`
text(220, pad + 8)`Pointy-top (Vertex)`
}
Four Pattern Types
All three grid functions share the same GridPatternType enum:
| Pattern | Visual | Description |
|---|---|---|
| Shape | Cell outlines | Complete grid lines — every edge drawn once |
| Dot | Small circles | Dots at every grid vertex |
| Intersection | Edge-aligned marks | Marks where edges meet — axis-aligned + for squares, 6-arm stars for triangles (3 bidirectional lines at 60°), 3-arm Y for hexagons |
| Partial | Centered segments | 40% of each edge length, centered on the edge midpoint |
Pattern proportions are relative to cellSize: dot radius is 2.5%, intersection arm length is 7.5%.
Putting It Together
Grid functions are most useful as background textures layered behind other geometry. Combine them with layer transforms for rotation, and use Color methods to derive palette variations from a single reactive base color:
// viewBox="0 0 400 400"
// Practical composition — rotated grid background with foreground geometry
let gridColor = Color(CSSVar('--grid-color', #5577aa));
let bgColor = Color(CSSVar('--bg-color', #0f172a));
// Background
let bg = PathLayer('bg') ${ fill: bgColor; stroke: none; };
bg.apply { rect(0, 0, 400, 400) }
// Layer 1: Rotated partial grid as background texture
let gridLayer = PathLayer('grid') ${ stroke: gridColor; stroke-width: 0.3; fill: none; stroke-linecap: round; };
gridLayer.ctx.transform.rotate.set(0.08pi);
gridLayer.apply {
squareGrid(GridPatternType.Partial, -40, -40, 480, 480, 16);
}
// Layer 2: Hex grid overlay with shape pattern
let accentColor = gridColor.lighten(30%);
let hexLayer = PathLayer('hex') ${ stroke: accentColor; stroke-width: 0.5; fill: none; };
hexLayer.apply {
hexagonGrid(GridPatternType.Shape, 60, 60, 280, 280, 30);
}
// Layer 3: Triangle intersection marks at center
let triColor = gridColor.hueShift(180).lighten(20%);
let triLayer = PathLayer('tri') ${ stroke: triColor; stroke-width: 0.6; fill: none; stroke-linecap: round; };
triLayer.apply {
triangleGrid(GridPatternType.Intersection, 120, 120, 160, 160, 25);
}
// Labels
let labels = TextLayer('labels') ${
font-family: system-ui, sans-serif;
font-size: 9;
fill: #64748b;
text-anchor: start;
};
labels.apply {
text(22, 388)`Rotated partial grid + hex outlines + triangle intersections`
}
The composition uses three techniques worth noting:
- Rotation via transforms —
gridLayer.ctx.transform.rotate.set(0.08pi)tilts the background grid. The grid area is oversized (-40, -40, 480, 480) to prevent gaps at the rotated corners. - Layered grid types — a partial square grid as subtle texture, hex outlines as mid-ground structure, and triangle intersection marks as a focal accent. Each gets its own
PathLayerwith distinct styling. - Color derivation — all three grid colors derive from a single
--grid-colorvariable via.lighten()and.hueShift(), so changing the base color recolors the entire composition.
The core pattern is always the same: create a styled PathLayer, optionally set a transform, then call the grid function inside layer.apply {}.
Three functions, four pattern types, two hexagon orientations — enough to cover graph paper, game boards, engineering overlays, and generative art backgrounds without writing a single loop. Try changing the --grid-color and --bg-color variables in any of the examples above to see how the reactive colors work.
For the full function signatures and parameter details, see the stdlib reference. For layer management and transforms, see the layers documentation. For combining grids with reusable path blocks, see the PathBlock introduction.