#!/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"(.*?)", src, re.S).group(1)
body = src[src.index("") + len(""):src.rindex("")]
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'''
'''
GRAIN = ''' '''
def blur(id_, sd):
return f' '
# --- 1. Graphite ---------------------------------------------------------------
def graphite():
d, b = shift_parts()
s = 6.2
tx, ty = W / 2 - 109 * s, H / 2 - 112 * s
defs = d + f'''
''' + GRAIN.format(a="0.07") + "\n" + blur("far", 60)
body = f''' {b}'''
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' '
f''
f''
f''
f''
f''
'\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 = [''' ''', 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''' ''')
# 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' \n',
f' \n']
for n, x0, y0, w, d, t, sx, sy, lvl, filt, alpha in bands:
body.append(f' \n' if filt else " \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' \n')
body.append(slab(x, y, w, d, t, None, alpha, f"d{min(4, lvl + i // 3)}"))
body.append(" \n")
body.append(f' ')
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'')
defs = '''
''' + GRAIN.format(a="0.06")
body = f''' {"".join(faces)}'''
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'')
slots = []
for k in range(12):
a0 = k * 30 - 10
if k == 1:
continue
slots.append(f'')
lit_slot = annulus(cx, cy, R + 120, R + 370, 30 - 10, 30 + 10)
defs = f'''
''' + GRAIN.format(a="0.05") + "\n" + blur("soft", 40)
body = f''' {"".join(slots)}{"".join(wedges)}'''
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"))