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
| # /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import math
import sys
import random
SATURATION = 64
LUMINANCE = 50
ALPHA = 0.2
WIDTH = 800
HEIGHT = 450
N_RINGS = 9
N_SPOKES = 24
CENTER_X = WIDTH // 2
CENTER_Y = HEIGHT // 2
MAX_R = min(WIDTH, HEIGHT) // 2 - 24
RING_STEP = MAX_R // N_RINGS
MAX_JITTER = RING_STEP // 6
MIN_DUR = 5.0
MAX_DUR = 15.0
BG = "#1a1a2e"
def palette(hue_seed, n=3, rng=None):
step = 360 / n
out = []
for i in range(n):
base = (hue_seed + i * step) % 360
jitter = rng.uniform(-20, 20) if rng else 0
h = (base + jitter) % 360
out.append(f"hsl({h:.0f},{SATURATION}%,{LUMINANCE}%)")
return out
def make_defs():
return (
"<defs>\n"
" <filter id='sh' x='-30%' y='-30%'"
" width='160%' height='160%'>\n"
" <feDropShadow dx='2' dy='3' stdDeviation='3'"
" flood-color='#000000' flood-opacity='0.6'/>\n"
" </filter>\n"
"</defs>"
)
def generate(seed: int = 42) -> str:
rng = random.Random(seed)
r = RING_STEP // 2
r = 24
hue_seed = rng.uniform(0, 360)
colors = palette(hue_seed, rng=rng)
parts = [
'<svg xmlns="http://www.w3.org/2000/svg"'
f' viewBox="0 0 {WIDTH} {HEIGHT}"'
f' width="{WIDTH}" height="{HEIGHT}">',
f'<style>:root{{width:100%;height:100%;background:{BG}}}</style>',
make_defs(),
f'<rect width="{WIDTH}" height="{HEIGHT}" fill="{BG}"/>',
]
points = [(CENTER_X, CENTER_Y)]
for ring in range(1, N_RINGS + 1):
ring_r = ring * RING_STEP
offset = math.pi / N_SPOKES if ring % 2 else 0
for spoke in range(N_SPOKES):
angle = 2 * math.pi * spoke / N_SPOKES + offset
points.append((
CENTER_X + ring_r * math.cos(angle),
CENTER_Y + ring_r * math.sin(angle),
))
for ocx, ocy in points:
dx = rng.uniform(-MAX_JITTER, MAX_JITTER)
dy = rng.uniform(-MAX_JITTER, MAX_JITTER)
cx = ocx + dx
cy = ocy + dy
color = rng.choice(colors)
dur = rng.uniform(MIN_DUR, MAX_DUR)
phase = rng.uniform(0, dur)
parts.append(
f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{r}"'
f' fill="{color}" fill-opacity="{ALPHA}"'
f' stroke="#000000" stroke-width="1.5"'
f' filter="url(#sh)">'
'<animateTransform'
' attributeName="transform" type="rotate"'
f' from="0 {ocx:.1f} {ocy:.1f}"'
f' to="360 {ocx:.1f} {ocy:.1f}"'
f' dur="{dur:.1f}s"'
f' begin="-{phase:.1f}s"'
' repeatCount="indefinite"/>'
'</circle>'
)
parts.append("</svg>")
return "\n".join(parts) + "\n"
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--output", "-o", default="cover.svg")
p.add_argument("--seed", type=int, default=82)
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)
|