tools/wallpapers.py writes them from the Shift geometry, so they follow the mark: Graphite (the default), Strata, Porcelain (light) and Dial. Each is a 3840x2160 SVG with a JPEG render beside it. JPEG is the right format for gradients under grain, and the branding README now says why this is the one exception to its no-JPEG rule. Removed: wallpaper1 (drawn flat, and built on the retired Guru mark), the site's unused copy of it, and the two superseded wordmark PNGs. The root README showed one of those, and now shows Graphite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
320 lines
18 KiB
Python
320 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
#
|
|
# The SchOS wallpapers, written out as SVG.
|
|
#
|
|
# tools/wallpapers.py [OUTDIR] (default: docs/branding/wallpapers)
|
|
#
|
|
# Generated rather than drawn, for the same reason the include rewrite is a
|
|
# script: every one of them is the Shift geometry from docs/branding/shift.svg
|
|
# under a different light, and a hand-edited copy of a path is a copy that
|
|
# drifts. Change the mark there and run this again.
|
|
#
|
|
# All of them are 3840x2160 and lit the way Niveus lights a key -- one light
|
|
# from the upper left, gloss on what faces it, a soft shadow on what is under
|
|
# it -- because they sit behind a desktop built from those keys.
|
|
#
|
|
# The PNGs beside them are renders of these files (see the README there); this
|
|
# script does not write them, since the only renderer that gets the filters
|
|
# right is a browser.
|
|
|
|
import math, os, re, sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "docs/branding/wallpapers")
|
|
W, H = 3840, 2160
|
|
|
|
# --- the mark -----------------------------------------------------------------
|
|
|
|
def shift_parts():
|
|
"""Return (defs, body) of shift.svg, ids prefixed so it can be embedded."""
|
|
src = open(os.path.join(ROOT, "docs/branding/shift.svg")).read()
|
|
defs = re.search(r"<defs>(.*?)</defs>", src, re.S).group(1)
|
|
body = src[src.index("</defs>") + len("</defs>"):src.rindex("</svg>")]
|
|
body = re.sub(r"<!--.*?-->", "", body, flags=re.S)
|
|
fix = lambda s: re.sub(r'id="([^"]+)"', r'id="sh-\1"', s).replace("url(#", "url(#sh-")
|
|
return fix(defs), fix(body)
|
|
|
|
# The three slabs as plain outlines, for the wallpapers that light the mark
|
|
# themselves rather than using its own shading.
|
|
SLABS = [ # (top face, front face, end wall), bottom slab first
|
|
("M54 126 L158 126 L128 156 L24 156 Z", "M24 156 L128 156 L128 167 L24 167 Z",
|
|
"M158 126 L158 137 L128 167 L128 156 Z"),
|
|
("M72 84 L176 84 L146 114 L42 114 Z", "M42 114 L146 114 L146 125 L42 125 Z",
|
|
"M176 84 L176 95 L146 125 L146 114 Z"),
|
|
("M90 42 L194 42 L164 72 L60 72 Z", "M60 72 L164 72 L164 83 L60 83 Z",
|
|
"M194 42 L194 53 L164 83 L164 72 Z"),
|
|
]
|
|
|
|
def svg(title, desc, defs, body):
|
|
return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">
|
|
<title>{title}</title>
|
|
<desc>{desc}</desc>
|
|
<defs>
|
|
{defs}
|
|
</defs>
|
|
{body}
|
|
</svg>
|
|
'''
|
|
|
|
GRAIN = ''' <filter id="grain" x="0" y="0" width="100%" height="100%">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" stitchTiles="stitch"/>
|
|
<feColorMatrix type="saturate" values="0"/>
|
|
<feComponentTransfer><feFuncA type="table" tableValues="0 {a}"/></feComponentTransfer>
|
|
</filter>'''
|
|
|
|
def blur(id_, sd):
|
|
return f' <filter id="{id_}" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="{sd}"/></filter>'
|
|
|
|
# --- 1. Graphite ---------------------------------------------------------------
|
|
|
|
def graphite():
|
|
d, b = shift_parts()
|
|
s = 6.2
|
|
tx, ty = W / 2 - 109 * s, H / 2 - 112 * s
|
|
defs = d + f'''
|
|
<radialGradient id="ground" cx="0.32" cy="0.18" r="1.05">
|
|
<stop offset="0" stop-color="#3A3530"/>
|
|
<stop offset="0.45" stop-color="#1E1B19"/>
|
|
<stop offset="1" stop-color="#0B0A09"/>
|
|
</radialGradient>
|
|
<radialGradient id="pool" cx="0.5" cy="0.5" r="0.5">
|
|
<stop offset="0" stop-color="#7C5CFF" stop-opacity="0.42"/>
|
|
<stop offset="0.5" stop-color="#5B3FC9" stop-opacity="0.14"/>
|
|
<stop offset="1" stop-color="#5B3FC9" stop-opacity="0"/>
|
|
</radialGradient>
|
|
<radialGradient id="vignette" cx="0.5" cy="0.5" r="0.75">
|
|
<stop offset="0.6" stop-color="#000" stop-opacity="0"/>
|
|
<stop offset="1" stop-color="#000" stop-opacity="0.55"/>
|
|
</radialGradient>
|
|
''' + GRAIN.format(a="0.07") + "\n" + blur("far", 60)
|
|
body = f''' <rect width="{W}" height="{H}" fill="url(#ground)"/>
|
|
<ellipse cx="{W/2+60}" cy="{H/2+330}" rx="1500" ry="520" fill="url(#pool)"/>
|
|
<ellipse cx="{W/2+90}" cy="{H/2+470}" rx="820" ry="70" fill="#000" opacity="0.55" filter="url(#far)"/>
|
|
<g transform="translate({tx:.1f} {ty:.1f}) scale({s})">{b}</g>
|
|
<rect width="{W}" height="{H}" fill="url(#vignette)"/>
|
|
<rect width="{W}" height="{H}" filter="url(#grain)"/>'''
|
|
return svg("SchOS — Graphite",
|
|
"Shift on warm graphite, lit from the upper left, casting a violet pool of light on the surface it stands on.",
|
|
defs, body)
|
|
|
|
# --- 2. Strata -----------------------------------------------------------------
|
|
|
|
def slab(x, y, w, d, t, shade, alpha, gid):
|
|
"""One slab in Shift's oblique: top face w wide and d deep, t thick."""
|
|
k = 30 / 104 * w * (d / (30 / 104 * w)) if w else 0
|
|
# the back edge is displaced right by the depth, as in the mark
|
|
top = f"M{x+d:.1f} {y:.1f} L{x+d+w:.1f} {y:.1f} L{x+w:.1f} {y+d:.1f} L{x:.1f} {y+d:.1f} Z"
|
|
front = f"M{x:.1f} {y+d:.1f} L{x+w:.1f} {y+d:.1f} L{x+w:.1f} {y+d+t:.1f} L{x:.1f} {y+d+t:.1f} Z"
|
|
wall = f"M{x+d+w:.1f} {y:.1f} L{x+d+w:.1f} {y+t:.1f} L{x+w:.1f} {y+d+t:.1f} L{x+w:.1f} {y+d:.1f} Z"
|
|
edge = f"M{x+0.6:.1f} {y+d-0.6:.1f} L{x+d+0.4:.1f} {y+0.6:.1f} L{x+d+w-1:.1f} {y+0.6:.1f}"
|
|
return (f' <g opacity="{alpha:.2f}">'
|
|
f'<path d="{top}" fill="url(#{gid}t)"/>'
|
|
f'<path d="{top}" fill="url(#gloss)"/>'
|
|
f'<path d="{front}" fill="url(#{gid}f)"/>'
|
|
f'<path d="{wall}" fill="url(#{gid}w)"/>'
|
|
f'<path d="{edge}" fill="none" stroke="#fff" stroke-opacity="0.5" stroke-width="{max(1.5, t/9):.1f}" stroke-linecap="round"/>'
|
|
'</g>\n')
|
|
|
|
def mix(a, b, f):
|
|
pa = [int(a[i:i+2], 16) for i in (1, 3, 5)]
|
|
pb = [int(b[i:i+2], 16) for i in (1, 3, 5)]
|
|
return "#%02X%02X%02X" % tuple(round(x + (y - x) * f) for x, y in zip(pa, pb))
|
|
|
|
def strata():
|
|
defs = [''' <linearGradient id="sky" x1="0" y1="0" x2="0.4" y2="1">
|
|
<stop offset="0" stop-color="#1A1438"/>
|
|
<stop offset="0.55" stop-color="#0C0A1C"/>
|
|
<stop offset="1" stop-color="#050409"/>
|
|
</linearGradient>
|
|
<radialGradient id="bloom" cx="0.5" cy="0.5" r="0.5">
|
|
<stop offset="0" stop-color="#8E72FF" stop-opacity="0.55"/>
|
|
<stop offset="1" stop-color="#7C5CFF" stop-opacity="0"/>
|
|
</radialGradient>
|
|
<linearGradient id="gloss" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0" stop-color="#fff" stop-opacity="0.34"/>
|
|
<stop offset="0.5" stop-color="#fff" stop-opacity="0.05"/>
|
|
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
|
|
</linearGradient>''', GRAIN.format(a="0.05"), blur("far", 7), blur("farther", 18), blur("shadow", 28)]
|
|
# Five depths of colour, from near (bright violet) to far (indigo, dim).
|
|
near, far = "#7C5CFF", "#2A2266"
|
|
for i in range(5):
|
|
c = mix(near, far, i / 4)
|
|
defs.append(f''' <linearGradient id="d{i}t" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="{mix(c, "#FFFFFF", 0.32)}"/><stop offset="0.55" stop-color="{c}"/><stop offset="1" stop-color="{mix(c, "#000000", 0.12)}"/></linearGradient>
|
|
<linearGradient id="d{i}f" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{mix(c, "#000000", 0.12)}"/><stop offset="1" stop-color="{mix(c, "#000000", 0.38)}"/></linearGradient>
|
|
<linearGradient id="d{i}w" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="{mix(c, "#000000", 0.35)}"/><stop offset="1" stop-color="{mix(c, "#000000", 0.55)}"/></linearGradient>''')
|
|
|
|
# A cascade: slabs stepping down and to the left, each displaced from the
|
|
# one above exactly as Shift's are, at three distances. The near ones are
|
|
# solid and cast a shadow on what is below them; the far ones are softer
|
|
# and dimmer, which is all depth of field is.
|
|
bands = [ # (count, x0, y0, width, depth, thickness, step x, step y, colour, blur, alpha)
|
|
(9, 1500, -60, 380, 95, 26, -150, 150, 4, "farther", 0.55),
|
|
(7, 3420, -40, 620, 150, 44, -230, 240, 2, "far", 0.8),
|
|
(5, 2650, 260, 980, 230, 74, -330, 340, 0, None, 1.0),
|
|
]
|
|
body = [f' <rect width="{W}" height="{H}" fill="url(#sky)"/>\n',
|
|
f' <ellipse cx="2300" cy="1150" rx="1900" ry="1100" fill="url(#bloom)"/>\n']
|
|
for n, x0, y0, w, d, t, sx, sy, lvl, filt, alpha in bands:
|
|
body.append(f' <g filter="url(#{filt})">\n' if filt else " <g>\n")
|
|
for i in range(n):
|
|
x, y = x0 + sx * i, y0 + sy * i
|
|
sh = f"M{x+d+40:.1f} {y+t+50:.1f} L{x+d+w+40:.1f} {y+t+50:.1f} L{x+w+40:.1f} {y+d+t+70:.1f} L{x+40:.1f} {y+d+t+70:.1f} Z"
|
|
body.append(f' <path d="{sh}" fill="#000" opacity="{0.45*alpha:.2f}" filter="url(#shadow)"/>\n')
|
|
body.append(slab(x, y, w, d, t, None, alpha, f"d{min(4, lvl + i // 3)}"))
|
|
body.append(" </g>\n")
|
|
body.append(f' <rect width="{W}" height="{H}" filter="url(#grain)"/>')
|
|
return svg("SchOS — Strata",
|
|
"Shift's slabs, repeated into a cascade that recedes into an indigo dark: near ones sharp and bright, far ones soft.",
|
|
"\n".join(defs), "".join(body))
|
|
|
|
# --- 3. Porcelain (light) ------------------------------------------------------
|
|
|
|
def porcelain():
|
|
s = 6.0
|
|
tx, ty = W / 2 - 109 * s, H / 2 - 108 * s
|
|
faces = []
|
|
for i, (top, front, wall) in enumerate(SLABS):
|
|
g = "pv" if i == 2 else "p" # the top slab carries the accent, as a glaze
|
|
faces.append(f'<path d="{top}" fill="url(#{g}t)"/><path d="{front}" fill="url(#{g}f)"/><path d="{wall}" fill="url(#{g}w)"/>')
|
|
defs = ''' <radialGradient id="paper" cx="0.3" cy="0.2" r="1.1">
|
|
<stop offset="0" stop-color="#F6F4EF"/>
|
|
<stop offset="0.6" stop-color="#E6E2DA"/>
|
|
<stop offset="1" stop-color="#D3CEC4"/>
|
|
</radialGradient>
|
|
<linearGradient id="pt" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#FFFFFF"/><stop offset="1" stop-color="#ECE8E0"/></linearGradient>
|
|
<linearGradient id="pf" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#D9D3C8"/><stop offset="1" stop-color="#C4BDB0"/></linearGradient>
|
|
<linearGradient id="pw" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#BDB5A7"/><stop offset="1" stop-color="#A9A092"/></linearGradient>
|
|
<linearGradient id="pvt" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#C9BBFF"/><stop offset="0.6" stop-color="#9479FF"/><stop offset="1" stop-color="#7C5CFF"/></linearGradient>
|
|
<linearGradient id="pvf" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#6E50E8"/><stop offset="1" stop-color="#5236C8"/></linearGradient>
|
|
<linearGradient id="pvw" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5338BC"/><stop offset="1" stop-color="#40289A"/></linearGradient>
|
|
<filter id="lift" x="-30%" y="-30%" width="160%" height="170%">
|
|
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
|
|
<feOffset dx="5" dy="9" result="o"/>
|
|
<feComponentTransfer><feFuncA type="linear" slope="0.28"/></feComponentTransfer>
|
|
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
</filter>
|
|
<filter id="bevel" x="-10%" y="-10%" width="120%" height="120%">
|
|
<feGaussianBlur in="SourceAlpha" stdDeviation="0.9" result="h"/>
|
|
<feSpecularLighting in="h" surfaceScale="2.5" specularConstant="0.7" specularExponent="18" lighting-color="#FFFFFF" result="s">
|
|
<feDistantLight azimuth="225" elevation="42"/>
|
|
</feSpecularLighting>
|
|
<feComposite in="s" in2="SourceAlpha" operator="in" result="sa"/>
|
|
<feComposite in="SourceGraphic" in2="sa" operator="arithmetic" k2="1" k3="0.6"/>
|
|
</filter>
|
|
<radialGradient id="accent" cx="0.5" cy="0.5" r="0.5">
|
|
<stop offset="0" stop-color="#7C5CFF" stop-opacity="0.22"/>
|
|
<stop offset="1" stop-color="#7C5CFF" stop-opacity="0"/>
|
|
</radialGradient>
|
|
''' + GRAIN.format(a="0.06")
|
|
body = f''' <rect width="{W}" height="{H}" fill="url(#paper)"/>
|
|
<ellipse cx="{W/2+120}" cy="{H/2+420}" rx="1100" ry="260" fill="url(#accent)"/>
|
|
<g transform="translate({tx:.1f} {ty:.1f}) scale({s})" filter="url(#lift)">
|
|
<g filter="url(#bevel)">{"".join(faces)}</g>
|
|
</g>
|
|
<rect width="{W}" height="{H}" filter="url(#grain)"/>'''
|
|
return svg("SchOS — Porcelain",
|
|
"The light one: Shift as glazed porcelain on soft paper, with the accent spent on the top slab alone.",
|
|
defs, body)
|
|
|
|
# --- 4. Dial ---------------------------------------------------------------------
|
|
|
|
def annulus(cx, cy, r0, r1, a0, a1):
|
|
"""An annular sector, angles in degrees clockwise from twelve o'clock."""
|
|
def pt(r, a):
|
|
t = math.radians(a)
|
|
return cx + r * math.sin(t), cy - r * math.cos(t)
|
|
(x0, y0), (x1, y1) = pt(r1, a0), pt(r1, a1)
|
|
(x2, y2), (x3, y3) = pt(r0, a1), pt(r0, a0)
|
|
big = 1 if a1 - a0 > 180 else 0
|
|
return (f"M{x0:.1f} {y0:.1f} A{r1} {r1} 0 {big} 1 {x1:.1f} {y1:.1f} "
|
|
f"L{x2:.1f} {y2:.1f} A{r0} {r0} 0 {big} 0 {x3:.1f} {y3:.1f} Z")
|
|
|
|
def dial():
|
|
cx, cy = W / 2, H / 2
|
|
R = 520 # the machined face
|
|
# Brushed metal, radially: thin wedges whose lightness follows the angle
|
|
# to the light, which is what makes a turned surface look turned.
|
|
wedges = []
|
|
# Each wedge overlaps the next by more than its own width, so no seam
|
|
# between two of them is ever a pixel of background -- which is what drew
|
|
# an interference cross through the middle of the first render.
|
|
for i in range(360):
|
|
a0, a1 = i * 1.0, i * 1.0 + 2.2
|
|
lit = 0.5 + 0.5 * math.cos(math.radians(2 * (a0 - 315)))
|
|
jitter = ((i * 7919) % 13) / 13 * 0.08
|
|
v = int(34 + 46 * lit + 30 * jitter)
|
|
wedges.append(f'<path d="{annulus(cx, cy, 0.1, R, a0, a1)}" fill="#{v:02X}{v:02X}{v+6:02X}"/>')
|
|
slots = []
|
|
for k in range(12):
|
|
a0 = k * 30 - 10
|
|
if k == 1:
|
|
continue
|
|
slots.append(f'<path d="{annulus(cx, cy, R + 120, R + 290, a0, a0 + 20)}"/>')
|
|
lit_slot = annulus(cx, cy, R + 120, R + 370, 30 - 10, 30 + 10)
|
|
defs = f''' <radialGradient id="room" cx="0.5" cy="0.45" r="0.8">
|
|
<stop offset="0" stop-color="#1D1A26"/>
|
|
<stop offset="0.6" stop-color="#0D0C12"/>
|
|
<stop offset="1" stop-color="#050507"/>
|
|
</radialGradient>
|
|
<linearGradient id="slot" gradientUnits="userSpaceOnUse" x1="{cx-1000}" y1="{cy-1000}" x2="{cx+1000}" y2="{cy+1000}">
|
|
<stop offset="0" stop-color="#4B3BA0"/><stop offset="0.5" stop-color="#30246E"/><stop offset="1" stop-color="#1C1548"/>
|
|
</linearGradient>
|
|
<linearGradient id="litslot" gradientUnits="userSpaceOnUse" x1="{cx+300}" y1="{cy-1100}" x2="{cx+700}" y2="{cy-700}">
|
|
<stop offset="0" stop-color="#C4B4FF"/><stop offset="0.5" stop-color="#7C5CFF"/><stop offset="1" stop-color="#5A3BE0"/>
|
|
</linearGradient>
|
|
<radialGradient id="gem" cx="0.38" cy="0.32" r="0.72">
|
|
<stop offset="0" stop-color="#F2EEFF"/><stop offset="0.25" stop-color="#A994FF"/>
|
|
<stop offset="0.7" stop-color="#7C5CFF"/><stop offset="1" stop-color="#3E24B0"/>
|
|
</radialGradient>
|
|
<radialGradient id="halo" cx="0.5" cy="0.5" r="0.5">
|
|
<stop offset="0" stop-color="#9F86FF" stop-opacity="0.6"/><stop offset="1" stop-color="#7C5CFF" stop-opacity="0"/>
|
|
</radialGradient>
|
|
<radialGradient id="facefall" cx="0.5" cy="0.5" r="0.5">
|
|
<stop offset="0.8" stop-color="#000" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity="0.45"/>
|
|
</radialGradient>
|
|
<linearGradient id="rim" x1="0" y1="0" x2="1" y2="1">
|
|
<stop offset="0" stop-color="#F0F0F6"/><stop offset="0.5" stop-color="#5A5A66"/><stop offset="1" stop-color="#1C1C22"/>
|
|
</linearGradient>
|
|
<filter id="bevel" x="-10%" y="-10%" width="120%" height="120%">
|
|
<feGaussianBlur in="SourceAlpha" stdDeviation="7" result="h"/>
|
|
<feSpecularLighting in="h" surfaceScale="5" specularConstant="0.9" specularExponent="28" lighting-color="#FFFFFF" result="s">
|
|
<feDistantLight azimuth="225" elevation="35"/>
|
|
</feSpecularLighting>
|
|
<feComposite in="s" in2="SourceAlpha" operator="in" result="sa"/>
|
|
<feComposite in="SourceGraphic" in2="sa" operator="arithmetic" k2="1" k3="0.35"/>
|
|
</filter>
|
|
<filter id="drop" x="-30%" y="-30%" width="160%" height="160%">
|
|
<feGaussianBlur in="SourceAlpha" stdDeviation="22"/>
|
|
<feOffset dx="14" dy="26"/>
|
|
<feComponentTransfer><feFuncA type="linear" slope="0.6"/></feComponentTransfer>
|
|
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
</filter>
|
|
''' + GRAIN.format(a="0.05") + "\n" + blur("soft", 40)
|
|
body = f''' <rect width="{W}" height="{H}" fill="url(#room)"/>
|
|
<circle cx="{cx}" cy="{cy}" r="{R+205}" fill="none" stroke="#000" stroke-opacity="0.55" stroke-width="240" filter="url(#soft)"/>
|
|
<g filter="url(#drop)"><g fill="url(#slot)" filter="url(#bevel)">{"".join(slots)}</g></g>
|
|
<circle cx="{cx + 0.5*(R+250)}" cy="{cy - 0.866*(R+250)}" r="380" fill="url(#halo)"/>
|
|
<g filter="url(#drop)"><path d="{lit_slot}" fill="url(#litslot)" filter="url(#bevel)"/></g>
|
|
<g filter="url(#drop)">
|
|
<circle cx="{cx}" cy="{cy}" r="{R+18}" fill="url(#rim)"/>
|
|
<g>{"".join(wedges)}</g>
|
|
<circle cx="{cx}" cy="{cy}" r="{R}" fill="url(#facefall)"/>
|
|
</g>
|
|
<circle cx="{cx}" cy="{cy}" r="330" fill="url(#halo)"/>
|
|
<circle cx="{cx}" cy="{cy}" r="176" fill="#120C2E"/>
|
|
<circle cx="{cx}" cy="{cy}" r="160" fill="url(#gem)"/>
|
|
<ellipse cx="{cx-50}" cy="{cy-62}" rx="56" ry="34" fill="#fff" opacity="0.7"/>
|
|
<rect width="{W}" height="{H}" filter="url(#grain)"/>'''
|
|
return svg("SchOS — Dial",
|
|
"The SchFS uberblock ring as a piece of hardware: a turned metal face, the slots as keys set around it, the newest one and the root lit.",
|
|
defs, body)
|
|
|
|
WALLS = {"graphite": graphite, "strata": strata, "porcelain": porcelain, "dial": dial}
|
|
|
|
if __name__ == "__main__":
|
|
os.makedirs(OUT, exist_ok=True)
|
|
for name, fn in WALLS.items():
|
|
with open(os.path.join(OUT, name + ".svg"), "w") as f:
|
|
f.write(fn())
|
|
print(os.path.join(OUT, name + ".svg"))
|