143 lines
6.7 KiB
Python
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)
|