Design: watching screen's programme bar and subtitles panel, every look

FigCraft proof (design/watching/build-watching-osd.py): the panel docks above
the bar, the bar stays while it is open, and every colour comes from the
look's tokens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-06 20:17:07 +00:00
co-authored by Claude Opus 5.5
parent 5bcf7a783e
commit d6461ff494
7 changed files with 91 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
The watching screen's programme bar and subtitles/sound panel, redesigned (Paul, 6 November 2026): the panel used to sit
halfway down the right and hang over the bar, the bar hid itself while the panel was open, and the bar's white and dark
blue ignored the theme. Here both are one family drawn only from each look's tokens (Themes.kt), the panel docked above
the bar on the right, the bar staying while the panel is open. Drawn as SVG, converted and exported with FigCraft.
"""
import subprocess
from html import escape
from pathlib import Path
HERE = Path(__file__).resolve().parent
LOOKS = {
'Orbit Night': dict(background='#071632', surface='#12356B', raised='#194C91', accent='#FFDE59', ink='#FFFFFF', muted='#B7CBE8', deep='#0C2148', onAccent='#071632'),
'Daybreak': dict(background='#E8F1FB', surface='#FFFFFF', raised='#D2E2F6', accent='#1E66D0', ink='#0E1E3C', muted='#4A5D7A', deep='#DCE8F6', onAccent='#FFFFFF'),
'Nebula': dict(background='#170B2E', surface='#3A1D6B', raised='#522D8F', accent='#FF7AC6', ink='#FFFFFF', muted='#C8B4E6', deep='#22113F', onAccent='#170B2E'),
'Retro Tube': dict(background='#1A120B', surface='#3B2A1A', raised='#574029', accent='#FFB347', ink='#FFECD2', muted='#D2B48C', deep='#241911', onAccent='#1A120B'),
'Deep Sea': dict(background='#04212B', surface='#0B4A5C', raised='#126378', accent='#6EF0D8', ink='#F0FFFC', muted='#A0D2D2', deep='#072C38', onAccent='#04212B'),
'High Contrast': dict(background='#000000', surface='#1A1A1A', raised='#333333', accent='#FFFF00', ink='#FFFFFF', muted='#DCDCDC', deep='#0D0D0D', onAccent='#000000'),
}
FONT = 'Barlow, DejaVu Sans, sans-serif'
def t(x, y, s, size, fill, weight=400, anchor='start', extra=''):
return f'<text x="{x}" y="{y}" font-family="{FONT}" font-size="{size}" font-weight="{weight}" fill="{fill}" text-anchor="{anchor}" {extra}>{escape(s)}</text>'
def frame(c, panel=True):
"""One 1920x1080 frame: a programme stand-in, the bar along the bottom, and the panel docked above it on the right."""
o = ['<rect width="1920" height="1080" fill="#20303A"/>',
'<defs><linearGradient id="pic" x2="1" y2="1"><stop stop-color="#3A5A6A"/><stop offset="1" stop-color="#14202A"/></linearGradient></defs>',
'<rect width="1920" height="1080" fill="url(#pic)"/>',
'<circle cx="700" cy="420" r="210" fill="#4E7484" opacity=".5"/><rect x="1100" y="260" width="420" height="420" rx="20" fill="#28404C" opacity=".6"/>',
# Subtitles on the picture, kept above the bar.
t(960, 742, 'I said we\'d be home by Christmas.', 44, '#FFFFFF', 600, 'middle', 'stroke="#000" stroke-width="3" paint-order="stroke"')]
# The bar: one card, deep at 94%, a hairline of raised, rounded 16.
x, y, w, h = 134, 800, 1652, 236
o.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="16" fill="{c["deep"]}" fill-opacity=".94" stroke="{c["raised"]}" stroke-width="2"/>')
o.append(t(x + 32, y + 50, '308', 34, c['accent'], 700) + t(x + 104, y + 50, 'BritX', 34, c['ink'], 700) + t(x + w - 32, y + 50, '21:30 Fri 6 Nov', 28, c['muted'], 400, 'end'))
o.append(f'<rect x="{x + 32}" y="{y + 76}" width="92" height="40" rx="8" fill="{c["accent"]}"/>' + t(x + 78, y + 106, 'NOW', 26, c['onAccent'], 700, 'middle'))
o.append(t(x + 144, y + 108, 'Hot Fuzz', 42, c['ink'], 700) + t(x + w - 32, y + 108, '21:00 to 23:05', 28, c['muted'], 400, 'end'))
o.append(f'<rect x="{x + 32}" y="{y + 132}" width="{w - 64}" height="8" rx="4" fill="{c["raised"]}"/><rect x="{x + 32}" y="{y + 132}" width="{(w - 64) * .24:.0f}" height="8" rx="4" fill="{c["accent"]}"/>')
o.append(t(x + 32, y + 178, 'NEXT 23:05 The World\'s End', 28, c['muted'], 600))
# Hints, on the surface along the bottom of the card.
o.append(f'<path d="M{x} {y + 196}H{x + w}V{y + h - 16}Q{x + w} {y + h} {x + w - 16} {y + h}H{x + 16}Q{x} {y + h} {x} {y + h - 16}Z" fill="{c["surface"]}"/>')
hints = [('▲▼', 'Channel'), ('◄', 'TV guide'), ('►', 'Subtitles and sound'), ('OK', 'Hide')] if not panel else [('▲▼', 'Move'), ('OK', 'Choose'), ('◄', 'Back'), ('►', 'Close')]
hx = x + 32
for key, label in hints:
kw = 22 * len(key) + 28
o.append(f'<rect x="{hx}" y="{y + 203}" width="{kw}" height="28" rx="6" fill="{c["raised"]}"/>' + t(hx + kw / 2, y + 225, key, 20, c['ink'], 700, 'middle') + t(hx + kw + 12, y + 225, label, 24, c['ink'], 600))
hx += kw + 30 + 14 * len(label)
if panel:
# The panel, docked above the bar's right end: never over it, never taller than the space above.
px, pw, ph = x + w - 520, 520, 600
py = y - 24 - ph
o.append(f'<rect x="{px}" y="{py}" width="{pw}" height="{ph}" rx="16" fill="{c["deep"]}" fill-opacity=".96" stroke="{c["raised"]}" stroke-width="2"/>')
o.append(t(px + 28, py + 52, 'SUBTITLES', 24, c['muted'], 700, extra='letter-spacing="3"'))
rows = [('Off', False, False), ('English', True, True), ('English (SDH)', False, False), ('French', False, False)]
ry = py + 72
for label, selected, cursor in rows:
if cursor: o.append(f'<rect x="{px + 16}" y="{ry}" width="{pw - 32}" height="52" rx="10" fill="{c["accent"]}"/>')
o.append(t(px + 44, ry + 36, ('✓ ' if selected else '') + label, 30, c['onAccent'] if cursor else c['ink'], 700 if (cursor or selected) else 400))
ry += 58
o.append(f'<rect x="{px + 28}" y="{ry + 8}" width="{pw - 56}" height="2" fill="{c["raised"]}"/>')
o.append(t(px + 28, ry + 56, 'SOUND', 24, c['muted'], 700, extra='letter-spacing="3"'))
ry += 74
for label, selected in [('English 5.1', True), ('English stereo', False), ('Commentary', False)]:
o.append(t(px + 44, ry + 36, ('✓ ' if selected else '') + label, 30, c['ink'], 700 if selected else 400)); ry += 58
return o
def board():
cells = []
for i, (name, c) in enumerate(LOOKS.items()):
gx, gy = (i % 2) * 980, (i // 2) * 600 + 140
cells.append(f'<g transform="translate({gx + 20} {gy}) scale(.5)">' + ''.join(frame(c)) + '</g>')
cells.append(t(gx + 20, gy + 568, name, 30, '#FFFFFF', 600))
head = t(40, 70, 'Watching: programme bar and subtitles panel', 46, '#FFDE59', 700) + t(40, 112, 'The panel docks above the bar and the bar stays while it is open. Every colour comes from the look.', 26, '#B7CBE8')
svg = f'<svg xmlns="http://www.w3.org/2000/svg" width="1980" height="1960" viewBox="0 0 1980 1960"><rect width="1980" height="1960" fill="#071632"/>{head}{"".join(cells)}</svg>'
(HERE / 'watching-osd-themes.svg').write_text(svg)
big = f'<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="2160" viewBox="0 0 1920 2160">{"".join(frame(LOOKS["Orbit Night"], panel=False))}<g transform="translate(0 1080)">{"".join(frame(LOOKS["Orbit Night"]))}</g></svg>'
(HERE / 'watching-osd-night.svg').write_text(big)
for name in ('watching-osd-themes', 'watching-osd-night'):
subprocess.run(['figcraft-cli', 'convert', str(HERE / f'{name}.svg'), str(HERE / f'{name}.figcraft')], check=True, stdout=subprocess.DEVNULL)
subprocess.run(['figcraft-cli', 'export', str(HERE / f'{name}.figcraft'), '--out', str(HERE / f'{name}.png')], check=True, stdout=subprocess.DEVNULL)
if __name__ == '__main__':
board()
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB