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)
|