Files
Raptor/docs/dialects/generate_border.py
2026-11-06 11:33:33 +01:00

143 lines
6.7 KiB
Python

"""Generate the repeating manuscript border: .venv/bin/python docs/dialects/generate_border.py."""
from math import atan2, cos, degrees, pi, sin
from pathlib import Path
import xml.etree.ElementTree as ET
OUTPUT = Path(__file__).with_name("jurassic-border.svg")
GOLD = "#c6a45f"
def generate():
parts = ['''<svg xmlns="http://www.w3.org/2000/svg" width="180" height="960" viewBox="0 0 180 960">
<title>Illuminated manuscript foliage border</title>
<desc>Procedural gold scrollwork, acanthus leaves, raptor medallions, rosettes, and pearl ornament on deep green.</desc>
<defs>
<linearGradient id="gold" x2="1" y2=".4">
<stop stop-color="#80602e"/><stop offset=".35" stop-color="#ead19a"/>
<stop offset=".6" stop-color="#b38a46"/><stop offset="1" stop-color="#e2bf73"/>
</linearGradient>
<linearGradient id="jade" x2="1" y2="1">
<stop stop-color="#749281"/><stop offset=".45" stop-color="#365d50"/><stop offset="1" stop-color="#152d27"/>
</linearGradient>
<linearGradient id="red" x2="1" y2="1">
<stop stop-color="#b86e60"/><stop offset=".5" stop-color="#793c39"/><stop offset="1" stop-color="#3e2527"/>
</linearGradient>
<g id="leaf" stroke="#c6a45f" stroke-width=".65" stroke-linejoin="round">
<path fill="url(#jade)" d="M0 0C-3-8-16-7-16-19Q-11-15-8-18C-17-24-14-34-12-38Q-8-29-4-31C-10-40-3-49 1-54C0-41 11-37 7-29Q14-32 15-39C23-26 15-21 10-18Q16-17 20-22C22-9 9-10 0 0Z"/>
<path d="M0-2Q-4-23 1-48M-2-12Q-11-14-13-20M-3-24Q-9-25-11-32M-2-34 0-41M-2-10Q11-11 16-17M-3-21Q9-22 13-31" fill="none"/>
<path d="M-5-13Q-9-17-9-19M1-27Q4-31 3-35" fill="none" stroke="#e6d2a0" stroke-width=".45"/>
</g>
<g id="bud" stroke="#c6a45f" stroke-width=".65">
<path d="M0 0Q-14-10-7-23Q-2-22 0-17Q2-22 7-23Q14-10 0 0Z" fill="url(#red)"/>
<path d="M0 0Q-5-13 0-27Q5-13 0 0Z" fill="url(#gold)"/>
<path d="M-6-6 0 0 6-6" fill="none"/>
</g>
<g id="raptor" stroke="#c6a45f" stroke-linejoin="round">
<circle r="46" fill="#090f0c" stroke-width="1.5"/>
<circle r="42" fill="#14221c" stroke-width=".5"/>
<circle r="39" fill="none" stroke-width=".5" stroke-dasharray="1 3"/>
<path d="M-26 29Q-15 12-24-1L-32-5-25-9-32-15-22-16-27-24-15-21-16-30-5-23Q7-26 17-15L34-9Q39-6 34 0L14 4 31 9Q33 13 26 16L6 12Q-2 17 0 31Z" fill="url(#jade)" stroke-width="1.2"/>
<path d="M-26 29Q-3 25-5 6Q-16-1-12-12Q-8-21 4-18M-15 28Q3 24 3 12M-22-15-13-10M-22-6-14-4M-17 3-10 5" fill="none" stroke-width=".65"/>
<path d="M-4-10Q3-18 12-10L5-5Z" fill="url(#gold)" stroke-width=".5"/>
<path d="M5-13 4-7" fill="none" stroke="#090f0c" stroke-width="2"/>
<path d="M13 4 17 8 19 3M23 2 25 6 28 1M12 13 15 9 18 14M23 15 25 11 28 14" fill="#ead7a5" stroke-width=".35"/>
<path d="M14-4Q25-7 31-4M5 1 10 2M-10 12-7 14M-13 19-10 21" fill="none" stroke-width=".55"/>
<circle cx="30" cy="-6" r="1.1" fill="#090f0c" stroke="none"/>
<path d="M-5-36 0-41 5-36 0-32ZM-5 36 0 41 5 36 0 32Z" fill="url(#red)" stroke-width=".65"/>
</g>
</defs>
<g stroke-linecap="round" stroke-linejoin="round">
<path d="M0 0H24V960H0Z" fill="#14221c"/>
<path d="M3 0V960M21 0V960" stroke="url(#gold)" stroke-width="1.4"/>
<path d="M6 0V960M18 0V960" stroke="#79633c" stroke-width=".4"/>
''']
def path(d, stroke=GOLD, width=.65, fill="none"):
parts.append(f'<path d="{d}" fill="{fill}" stroke="{stroke}" stroke-width="{width}"/>')
def circle(x, y, r, fill=GOLD):
parts.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="{r}" fill="{fill}"/>')
def use(name, x, y, angle=0, scale=1):
parts.append(f'<use href="#{name}" transform="translate({x:.2f} {y:.2f}) rotate({angle:.2f}) scale({scale:.2f})"/>')
# All vertical periods divide 960, including the scrolls across the tile joins.
for y in range(-24, 985, 24):
path(f"M12 {y-9} 16 {y} 12 {y+9} 8 {y}Z", width=.5)
circle(12, y, 1.2, "#dec58e")
for y in range(-240, 961, 240):
d = f"M88 {y}C150 {y+60} 26 {y+180} 88 {y+240}"
path(d, "#594525", 5)
path(d, "url(#gold)", 3)
path(d, "#f0dab0", .55)
for side, cy in ((1, y+63), (-1, y+183)):
cx = 88 + side*20
points = []
for i in range(97):
t = i / 96
angle = -pi/2 + side*t*2*pi*1.35
radius = 42*(1-t)+2
points.append((cx+radius*cos(angle), cy+radius*sin(angle)))
d = "M" + "L".join(f"{x:.2f} {yy:.2f}" for x, yy in points)
path(d, "#594525", 2.8)
path(d, "url(#gold)", 1.6)
for i in (8, 22, 36, 50):
x, yy = points[i]
nx, ny = points[i+1]
angle = degrees(atan2(ny-yy, nx-x)) + (145 if side == 1 else 35)
use("leaf", x, yy, angle, .48 if i == 50 else .63)
x, yy = points[-1]
circle(x, yy, 3.3, "#542f2e")
circle(x, yy, 1.5, "#e4c783")
for offset, side in ((16, -1), (108, 1), (136, 1), (228, -1)):
yy = y+offset
x = 88 + 18*sin(2*pi*offset/240)
use("leaf", x, yy, side*56, .84)
tx, ty = x+side*39, yy-20
path(f"M{x:.2f} {yy}Q{tx:.2f} {yy+6} {tx:.2f} {ty}", width=.8)
use("bud", tx, ty, side*25, .55)
# Small rosettes and pen flourishes fill the spaces between the main scrolls.
for x, yy in ((45, y+91), (139, y+211)):
for angle in range(0, 360, 45):
a = angle*pi/180
parts.append(f'<ellipse cx="0" cy="-5" rx="2.4" ry="5" fill="url(#red)" stroke="{GOLD}" stroke-width=".5" transform="translate({x} {yy}) rotate({angle})"/>')
circle(x+13*cos(a), yy+13*sin(a), .65)
circle(x, yy, 3.2, "#d7b66e")
circle(x, yy, 1.1, "#f5e0ac")
for side in (-1, 1):
path(f"M{x+side*3} {yy+15}q{side*12} 10 {side*6} 20q{-side*5} 5 {-side*7}-1q0-4 {side*4}-2", width=.55)
for i in range(24):
yy = y+i*10
x = 158+5*sin(i*pi/6)
circle(x, yy, .7 if i % 3 else 1.1, "#a58951")
for yy in (y+20, y+140):
path(f"M151 {yy-5}V{yy+5}M147 {yy}H155", width=.7)
for y in (120, 600):
use("raptor", 92, y, scale=.92)
for angle in range(0, 360, 30):
a = angle*pi/180
circle(92+45*cos(a), y+45*sin(a), 1.1, "#e4c783")
parts.append("</g></svg>\n")
return "\n".join(parts)
if __name__ == "__main__":
document = generate()
root = ET.fromstring(document)
assert root.attrib["viewBox"] == "0 0 180 960"
ids = {element.attrib["id"] for element in root.iter() if "id" in element.attrib}
for element in root.iter():
if "href" in element.attrib:
assert element.attrib["href"][1:] in ids
assert document == generate(), "Border generation must be deterministic"
OUTPUT.write_text(document)