#!/usr/bin/env python3 """Orbit's next interface: custom icons, main menu, grid guide and watching screen. Writes SVG sources, then native VectorCraft icons and FigCraft screens. Programmes shown are fictional. Yellow stays reserved for remote focus, as in the existing app. """ from pathlib import Path import base64, glob, html, math, os, re, subprocess P = Path(__file__).resolve().parent ROOT = P.parent.parent COLLECTION = ROOT.parent / '2026-10-31/referenced-chatgpt-conversation-this-is-an/Orbit-Channel-Designs' ENV = dict(os.environ, XDG_DATA_HOME=str(ROOT / '.tooling/font-data')) NAVY, PANEL, BLUE, FOCUS, MUTED, SILVER = '#071632', '#12356B', '#194C91', '#FFDE59', '#B7CBE8', '#E8EDF5' (P / 'icons').mkdir(exist_ok=True); (P / 'sources').mkdir(exist_ok=True); (P / 'review').mkdir(exist_ok=True) def svg(w, h, body): return f'{body}' def text(x, y, value, size=24, fill='white', weight='400', family='Barlow', anchor='start', extra=''): return f'{html.escape(value)}' def rect(x, y, w, h, fill, r=0, extra=''): return f'' def gear_path(): points, teeth = [], 8 for i in range(teeth * 4): a = 2 * math.pi * i / (teeth * 4) - math.pi / 8 r = 40 if i % 4 in (0, 1) else 31 points.append(f'{48 + r * math.cos(a):.1f},{48 + r * math.sin(a):.1f}') return ' '.join(points) def icon(name, c, accent): s = f'fill="none" stroke="{c}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"' a = f'fill="none" stroke="{accent}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"' return { 'all': f'', 'entertainment': f'', 'movies': f'', 'comedy': f'', 'kids': f'', 'documentaries': f'', 'sport': f'', 'gear': f'', 'profile': f'', 'office': f'', }[name] ICONS = ['all', 'entertainment', 'movies', 'comedy', 'kids', 'documentaries', 'sport', 'gear', 'office', 'profile'] for name in ICONS: src = P / 'icons' / f'orbit_icon_{name}.svg' src.write_text(svg(96, 96, icon(name, SILVER, '#7FB2FF'))) for out, options in ((src.with_suffix('.vectorcraft'), []), (src.with_suffix('.png'), ['--scale', '2'])): subprocess.run(['vectorcraft-cli', 'convert', str(src), str(out), *options], env=ENV, check=True, stdout=subprocess.DEVNULL) # Orbit's own mark: the same crescent and orbit every channel carries, in the network's blue and silver. ORBIT_DEFS = '' ORBIT_MARK = '' def orbit_logo(x, y, height): """Mark and wordmark, height tall. Drawn 640 by 160.""" return (f'' + ORBIT_DEFS + f'{ORBIT_MARK}' + text(168, 118, 'ORBIT', 104, 'url(#orbitsilver)', '500', 'Barlow Condensed', extra='letter-spacing="14"') + '') for name, body in (('orbit_logo', svg(640, 160, orbit_logo(0, 0, 160))), ('orbit_mark', svg(512, 512, ORBIT_DEFS + f'{ORBIT_MARK}'))): src = P / 'icons' / f'{name}.svg' src.write_text(body) for out, options in ((src.with_suffix('.vectorcraft'), []), (src.with_suffix('.png'), [])): subprocess.run(['vectorcraft-cli', 'convert', str(src), str(out), *options], env=ENV, check=True, stdout=subprocess.DEVNULL) def use_icon(name, x, y, size, colour=SILVER, accent='#7FB2FF'): return f'{icon(name, colour, accent)}' counter = [0] def logo(folder, x, y, width): """Inline a channel's outlined guide logo, with its ids made unique for this page.""" counter[0] += 1 found = glob.glob(str(COLLECTION / folder / 'exports/*_epg.svg'))[0] s = open(found).read() inner = s[s.index('>', s.index('')] for name in set(re.findall(r'id="([^"]+)"', inner)): inner = inner.replace(f'id="{name}"', f'id="l{counter[0]}{name}"').replace(f'url(#{name})', f'url(#l{counter[0]}{name})').replace(f'href="#{name}"', f'href="#l{counter[0]}{name}"') return f'{inner}' def still(): out = P / 'sources/still.jpg' subprocess.run(['ffmpeg', '-y', '-v', 'error', '-ss', '1.2', '-i', str(COLLECTION / 'depth-native/city/heroes/heroes_city.mp4'), '-frames:v', '1', '-q:v', '3', str(out)], check=True) return 'data:image/jpeg;base64,' + base64.b64encode(out.read_bytes()).decode() STILL = still() def picture(x, y, w, h): return f'' def top_bar(title='orbit'): return (orbit_logo(60, 26, 76) + text(1656, 74, 'Sun 1 Nov 20:14', 28, 'white', '600', anchor='end') + rect(1688, 28, 76, 68, PANEL, 12) + use_icon('gear', 1700, 36, 52) + rect(1784, 28, 76, 68, PANEL, 12) + use_icon('profile', 1798, 38, 48)) def legend(items, y=1040, x=60): out = '' for key, label in items: out += text(x, y, key, 24, 'white', '600') x += 18 + (32 * len(key) if key[0] in '▲▼◄►' else 15 * len(key)) out += text(x, y, label, 22, MUTED) x += 40 + 11 * len(label) return out def category_rows(selected, y=132, x=60, w=350): out = '' for i, (name, label) in enumerate([('all', 'All Channels'), ('entertainment', 'Entertainment'), ('movies', 'Movies'), ('comedy', 'Comedy'), ('kids', 'Kids'), ('documentaries', 'Documentaries'), ('sport', 'Sport')]): ry = y + i * 106 on = i == selected out += rect(x, ry, w, 94, FOCUS if on else PANEL, 10) out += use_icon(name, x + 16, ry + 19, 56, NAVY if on else SILVER, NAVY if on else '#7FB2FF') out += text(x + 90, ry + 58, label, 30, NAVY if on else 'white', '600') return out def progress(x, y, w, fraction): return rect(x, y, w, 8, '#0B2452', 4) + rect(x, y, w * fraction, 8, SILVER, 4) # Main menu: television first. One live picture, and two small promises beside it. home = rect(0, 0, 1920, 1080, NAVY) + top_bar() + category_rows(0) home += picture(440, 132, 860, 484) + rect(440, 132, 860, 484, 'none', 0, f'stroke="{BLUE}" stroke-width="3"') home += rect(440, 628, 860, 234, PANEL, 10) + logo('heroes', 456, 650, 260) home += text(736, 676, 'NOW 20:00 to 20:30', 24, MUTED, '600') + text(736, 726, 'The Night Watch', 44, 'white', '600') + progress(736, 750, 540, .47) + text(736, 806, 'NEXT 20:30 City Under Siege', 26, MUTED) home += text(1330, 158, 'On now', 28, 'white', '600') for i, (folder, title, frac) in enumerate([('orbit-terra', 'Life on the Ice Shelf', .62), ('paradox', 'The Signal Room', .2), ('orbit-gold', 'Mind the Shop', .81)]): ry = 176 + i * 112 home += rect(1330, ry, 530, 102, PANEL, 10) + logo(folder, 1338, ry + 16, 220) + text(1566, ry + 46, title, 25, 'white', '600') + progress(1566, ry + 66, 276, frac) home += rect(1330, 522, 530, 340, PANEL, 10) + rect(1330, 522, 530, 62, '#7A5A14', 10) + rect(1330, 560, 530, 24, '#7A5A14') home += use_icon('office', 1344, 530, 46, '#FFF3C4', '#FFF3C4') + text(1402, 564, 'Tonight at OrbitOffice', 28, '#FFF3C4', '600') home += text(1356, 644, 'Late Night Spooks', 50, 'white', '500', 'Barlow Condensed') + text(1356, 692, 'Three films from 21:00', 26, MUTED) home += text(1356, 748, 'The Last Screening', 25, 'white') + text(1356, 786, 'Midnight Visitors', 25, 'white') + text(1356, 824, 'The Hollow House', 25, 'white') home += legend([('▲▼', 'Move'), ('OK', 'Select'), ('►', 'Picture and events'), ('Back', 'Full screen')], 960) (P / 'sources/orbit_main_menu.svg').write_text(svg(1920, 1080, home)) # Grid guide: picture one side, the highlighted programme the other, channels and times below. guide = rect(0, 0, 1920, 1080, NAVY) + top_bar() guide += picture(60, 116, 576, 324) + rect(60, 116, 576, 324, 'none', 0, f'stroke="{BLUE}" stroke-width="3"') guide += logo('orbit-odyssey', 660, 112, 300) + text(660, 240, 'Under Northern Stars', 52, 'white', '600') guide += text(660, 286, '20:30 to 21:30 • Sun 1 Nov • Orbit Odyssey', 26, MUTED, '600') guide += text(660, 336, 'A crew of astronomers follow a once-in-a-century comet across the Arctic,', 27, 'white') + text(660, 372, 'racing the polar night to capture it from the top of the world.', 27, 'white') guide += text(660, 422, 'OK Watch • Hold OK Go to channel number', 23, MUTED) gx, gy, rowh, left = 400, 462, 90, 60 guide += rect(left, gy, 330, 48, PANEL) + text(left + 18, gy + 34, 'All Channels', 26, 'white', '600') + use_icon('all', left + 278, gy + 6, 36) span = 1860 - gx times = ['20:00', '20:30', '21:00', '21:30'] guide += rect(gx - 10, gy, span + 10, 48, PANEL) for i, t in enumerate(times): guide += text(gx + i * span / 4 + 10, gy + 34, t, 25, 'white', '600') rows = [(1, 'orbit-1', [(0, 1, 'The Evening Exchange'), (1, 2, 'Family Favourites'), (3, 1, 'Home Run')]), (21, 'heroes', [(0, 1, 'The Night Watch'), (1, 1, 'City Under Siege'), (2, 2, 'The Silver Shield')]), (34, 'orbit-odyssey', [(0, 1, 'Maps of the Deep'), (1, 2, 'Under Northern Stars'), (3, 1, 'Engines')]), (40, 'orbit-terra', [(0, 2, 'Life on the Ice Shelf'), (2, 2, 'The Long Migration')]), (52, 'paradox', [(0, 1, 'The Signal Room'), (1, 1, 'Mirror Town'), (2, 1, 'The Static Hour'), (3, 1, 'Lost Hours')])] for i, (number, folder, shows) in enumerate(rows): ry = gy + 56 + i * rowh guide += rect(left, ry, 330, rowh - 8, PANEL) + text(left + 16, ry + 52, f'{100 + number}', 26, MUTED, '600') + logo(folder, left + 66, ry + 2, 250) for start, length, title in shows: focused = title == 'Under Northern Stars' x = gx + start * span / 4 width = length * span / 4 - 6 guide += rect(x, ry, width, rowh - 8, FOCUS if focused else (BLUE if start == 0 else '#102D5E')) guide += text(x + 16, ry + 50, title, 27, NAVY if focused else 'white', '600' if focused else '400') now_x = gx + span * .47 / 2 guide += rect(now_x - 1, gy + 48, 3, 5 * rowh + 2, '#FFFFFF', 0, 'opacity=".85"') guide += legend([('▲▼', 'Channel'), ('◄►', 'Time'), ('OK', 'Watch'), ('Hold OK', 'Number'), ('Back', 'Categories')], 1050) (P / 'sources/orbit_grid_guide.svg').write_text(svg(1920, 1080, guide)) # Watching: the classic bar on OK, and the small number box on hold. paper = SILVER watch = picture(0, 0, 1920, 1080) watch += text(1820, 110, '134', 84, '#47FF52', '700', 'monospace', 'end') watch += rect(760, 90, 400, 170, NAVY, 14, f'stroke="{FOCUS}" stroke-width="3"') + text(960, 136, 'Go to channel', 28, MUTED, '600', anchor='middle') for i, d in enumerate(['1', '3', '_']): watch += rect(838 + i * 84, 156, 72, 84, FOCUS if i == 2 else PANEL, 8) + text(874 + i * 84, 216, d, 54, NAVY if i == 2 else 'white', '600', anchor='middle') watch += rect(132, 780, 1656, 46, paper) + text(158, 812, '134 Orbit Odyssey', 25, NAVY, '600') + text(1762, 812, '20:42 Sun 1 Nov', 24, NAVY, anchor='end') watch += rect(132, 826, 1656, 128, BLUE) + text(160, 876, 'NOW', 24, FOCUS, '600') + text(290, 876, 'Under Northern Stars', 38, 'white', '600') + progress(1270, 866, 490, .2) watch += text(160, 930, 'NEXT', 22, MUTED, '600') + text(290, 930, '21:30 Engines', 27, 'white') watch += rect(132, 954, 1656, 58, paper) x = 160 for key, label in [('▲▼', 'Channel'), ('◄', 'Last channel'), ('►', 'Subtitles'), ('Hold OK', 'Number'), ('Back', 'TV guide')]: watch += text(x, 994, key, 25, NAVY, '600'); x += 18 + (32 * len(key) if key[0] in '▲▼◄►' else 16 * len(key)) watch += text(x, 994, label, 25, NAVY, '600'); x += 60 + 13 * len(label) (P / 'sources/orbit_watching.svg').write_text(svg(1920, 1080, watch)) for name in ('orbit_main_menu', 'orbit_grid_guide', 'orbit_watching'): native = P / 'review' / f'{name}.figcraft' subprocess.run(['figcraft-cli', 'convert', str(P / 'sources' / f'{name}.svg'), str(native)], env=ENV, check=True, stdout=subprocess.DEVNULL) subprocess.run(['figcraft-cli', 'export', str(native), '--out', str(native.with_suffix('.png'))], env=ENV, check=True, stdout=subprocess.DEVNULL) print('built')