Featured image of post Artboard 1

Artboard 1

Procedural SVG tile art generated from ~80 lines of pure Python.

About

Each tile in this image is drawn by the same short algorithm, seeded once and run independently per cell. There are no external dependencies — just Python’s built-in random module and string formatting.

Fourfold symmetry. Every tile is divided into four quadrants. The algorithm draws a set of line segments into one quadrant, then rotates that group by 90°, 180°, and 270° around the tile centre to fill the rest. This guarantees the final pattern has four-way rotational symmetry regardless of what the random lines look like.

Lines in a triangle. Within each quadrant, lines are placed on an even sub-grid (sections × sections squares). Starting points are constrained to the lower-right triangle (y ≤ x), which stops the four rotations from overlapping at the diagonal. Each line also gets a mirror copy reflected across the quadrant’s main diagonal, doubling the density without extra sampling.

pos (gradient direction). Lines either run up-right (positive slope) or down-right (negative slope). The pos flag is True only at specific interior grid positions — a deliberate quirk from the original that gives the pattern its mix of crossing angles rather than all lines running the same way.

Fixed seed, stable output. generate(seed=42) produces the same image on every build. Pass --seed N on the command line to explore variants.

Generator

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import sys
import random


def draw_line(
    x, y, length, pos, size, top_left_x, top_left_y, sw,
    sym=False, color="rosybrown",
):
    """Return the SVG XML for a single line segment.

    x, y     — grid coordinates of the starting point
    length   — number of grid squares to span
    pos      — True for positive gradient (up-right), False for down-right
    size     — pixel size of one grid square
    top_left_x/y — pixel origin of the tile
    sw       — stroke width
    sym      — True for the diagonal mirror copy of a line
    color    — stroke colour
    """
    # mirror: negate length so the reflected line runs the other direction
    if sym:
        length = -length
    x2 = x + length
    y2 = y - length if pos else y + length

    # clamp endpoints that overshoot the tile edge back to the boundary
    if x2 < 0:
        y2 += x2
        x2 = 0
    if y2 < 0:
        x2 += y2
        y2 = 0
    # swap coords: mirrored+positive case lands in the right quadrant
    if sym and pos:
        x2, y2 = y2, x2

    return (
        f'<line x1="{top_left_x + x * size}" y1="{top_left_y + y * size}"'
        f' x2="{top_left_x + x2 * size}" y2="{top_left_y + y2 * size}"'
        f' stroke-linecap="square" stroke-width="{sw}" stroke="{color}" />'
    )


def create_lines(sections, rng):
    # Pick a random number of line segments to draw in one quadrant of the tile.
    # Lines live in the lower-right diagonal half of the bottom-right quadrant;
    # the fourfold rotation then fills the rest of the tile.
    num_lines = rng.randint(sections // 2, int(sections * 1.5))
    point_set = set()
    lines = []
    # keep sampling until we have enough unique starting points
    while len(lines) < num_lines:
        # even grid coordinates only, so lines snap to the quadrant's sub-grid
        x = rng.randint(0, sections // 2 - 1) * 2
        # y ≤ x: keeps us in the lower-right triangle
        y = rng.randint(0, x // 2) * 2

        # maximum span before the line exits the tile
        top = sections - x - y if x == y and x == 2 else sections - x
        length = rng.randint(1, top)
        # positive gradient only at specific interior grid positions
        pos = x in (2, 4) and y in (2, 4)

        if (x, y) not in point_set:
            lines.append((x, y, length, pos))
            point_set.add((x, y))
    return lines


def make_tiles(sections, x_tiles, y_tiles, tile_size, rng, sw=2):
    # sections — sub-grid divisions per quadrant (controls intricacy)
    # x_tiles/y_tiles — number of tiles across and down
    # tile_size — pixels per tile (tiles are square)
    # sw — stroke width
    width = x_tiles * tile_size
    height = y_tiles * tile_size
    square_size = tile_size / sections / 2  # pixel size of one sub-grid square
    parts = [
        f'<svg xmlns="http://www.w3.org/2000/svg"'
        f' viewBox="0 0 {width} {height}" width="{width}" height="{height}">\n',
        f'<rect width="{width}" height="{height}" fill="#111" />\n',
    ]
    for row in range(y_tiles):
        for col in range(x_tiles):
            top_left_x = col * tile_size
            top_left_y = row * tile_size
            cx = top_left_x + tile_size / 2  # rotation centre of this tile
            cy = top_left_y + tile_size / 2

            # build one quadrant's lines, then rotate into all four
            group = ""
            for x, y, length, pos in create_lines(sections, rng):
                group += (
                    draw_line(
                        x, y, length, pos, square_size,
                        top_left_x, top_left_y, sw,
                    ) + "\n"
                )
                # mirror across the diagonal to fill the other half
                if x != y or pos:
                    group += (
                        draw_line(
                            y, x, -length, pos, square_size,
                            top_left_x, top_left_y, sw, sym=True,
                        ) + "\n"
                    )

            # repeat this quadrant pattern at 0°, 90°, 180°, 270°
            for quad in range(4):
                rot = f"rotate({quad * 90} {cx} {cy})"
                parts.append(f'<g transform="{rot}">{group}</g>')

    parts.append("</svg>\n")
    return "".join(parts)


def generate(seed: int = 42) -> str:
    rng = random.Random(seed)
    return make_tiles(
        sections=10, x_tiles=4, y_tiles=3, tile_size=150, rng=rng, sw=2,
    )


if __name__ == "__main__":
    import argparse

    p = argparse.ArgumentParser()
    p.add_argument("--output", "-o", default="-")
    p.add_argument("--seed", type=int, default=42)
    args = p.parse_args()
    svg = generate(seed=args.seed)
    if args.output == "-":
        sys.stdout.write(svg)
    else:
        with open(args.output, "w") as f:
            f.write(svg)
Built with Hugo
Theme Stack designed by Jimmy