diff --git a/design/squeeze-back/build-squeeze-back.py b/design/squeeze-back/build-squeeze-back.py new file mode 100644 index 0000000..ab860ac --- /dev/null +++ b/design/squeeze-back/build-squeeze-back.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""The credits squeeze-back and cast promos, design proposal (Paul, 6 November 2026, after NostalgiaTV's v0.10.51). Over a +programme's last minute the picture eases into a smaller frame and "Next on " fills the rest of the screen, with a +cast member; then the channel's ident plays as now. A marathon look for one-show runs, and a cast promo between programmes. +Drawn with the first-run design's helpers and the cast's model sheet. For approval only.""" +import ast +from pathlib import Path +HERE = Path(__file__).resolve().parent +SRC = HERE.parent / 'multiroom' / 'build-multiroom.py' +tree = ast.parse(SRC.read_text()) +keep = [] +for n in tree.body: + if isinstance(n, ast.FunctionDef) and n.name == 'tv': break + if isinstance(n, ast.Expr) and isinstance(n.value, ast.Constant): continue + keep.append(n) +ns = {'__file__': str(SRC)} +exec(compile(ast.Module(keep, []), str(SRC), 'exec'), ns) +globals().update({k: v for k, v in ns.items() if k not in ('HERE', '__file__', 'save', 'OUT')}) +G = ns['g'] +def save(name): G['im'].resize((W, H), Image.LANCZOS).save(HERE / name); print(name) +def D(): return G['d'] +GUIDE = HERE.parent / 'interface' / 'review' / 'orbit_grid_guide.png' + +def frame(x, y, w, h, credits=True): + """The programme playing in its smaller frame: the guide mock-up's city scene, with rolling credits over it.""" + src = Image.open(GUIDE).convert('RGB').crop((64, 120, 634, 438)).resize((S(w), S(h)), Image.LANCZOS) + dark = Image.new('RGB', src.size, (0, 0, 0)); src = Image.blend(src, dark, .55 if credits else 0) + G['im'].paste(src, (S(x), S(y))) + if credits: + for i, line in enumerate(('Directed by', 'J. RANDOM', '', 'Produced by', 'A. N. OTHER', '', 'Music by', 'THE HOUSE BAND')): + text((x + w / 2, y + 40 + i * 34), line, semi(24) if line.isupper() else reg(20), WHITE if line.isupper() else MUTED, 'ma') + D().rounded_rectangle((S(x), S(y), S(x + w), S(y + h)), S(10), outline=(255, 255, 255), width=SW(3)) + +def art(x, y, w, h, title, colour): + """Programme artwork stand-in: a tinted card with the title, where the real poster or backdrop goes.""" + box(x, y, w, h, fill=colour, r=10) + for k in range(6): D().line([(S(x), S(y + h - k * h / 6)), (S(x + w), S(y + h - k * h / 6 - 40))], fill=tuple(min(255, c + 18) for c in colour), width=SW(2)) + text((x + 24, y + h - 70), title, anton(40), WHITE) + +# 1. The squeeze-back. +screen() +box(0, 0, W, H, fill=(9, 24, 56), r=0) +for _ in range(1): pass +frame(70, 120, 1000, 562) +logo(RES / 'paradox_epg.webp', 70, 40, 220) +text((1140, 120), 'NEXT ON PARADOX', anton(40), YELLOW) +text((1140, 180), 'in 45 seconds', reg(28), MUTED) +art(1140, 230, 700, 300, 'DOCTOR WHO', (60, 40, 110)) +text((1140, 560), 'S3 E9 · The Family of Blood', semi(36), WHITE) +text((1140, 610), 'Part 2 of 2: carries straight on from tonight', reg(26), MUTED) +box(70, 730, 1780, 180, fill=PANEL, r=16) +zip_((190, 820), 60, (255, 79, 163)) +text((290, 760), 'LATER ON PARADOX', semi(26), MUTED) +for i, (t, n) in enumerate((('20:45', 'The X-Files'), ('21:30', 'Fringe'), ('22:15', 'Buffy the Vampire Slayer'))): + text((290 + i * 520, 805), t, semi(34), YELLOW); text((400 + i * 520, 805), n, semi(32), WHITE) +text((290, 860), 'The ident follows as usual when the credits end.', reg(24), MUTED) +motion(1140, 940, 700, 120, ['Over the last minute the picture eases into', 'the frame; Zip whizzes in. Instant with Reduce motion.']) +save('squeeze-back-1-next.png') + +# 2. The marathon look: one show running on. +screen() +box(0, 0, W, H, fill=(9, 24, 56), r=0) +frame(70, 120, 1000, 562) +logo(RES / 'orbit_gold_epg.webp', 70, 40, 220) +text((1140, 120), 'KEEP WATCHING', anton(40), YELLOW) +text((1140, 180), 'Fawlty Towers marathon · episode 3 of 6', reg(28), MUTED) +art(1140, 230, 700, 300, 'FAWLTY TOWERS', (110, 60, 30)) +text((1140, 560), 'S1 E4 · The Hotel Inspectors', semi(36), WHITE) +text((1140, 610), 'Straight after the credits', reg(26), MUTED) +box(70, 730, 1780, 180, fill=PANEL, r=16) +for i in range(6): + x = 290 + i * 250 + box(x, 790, 220, 60, fill=YELLOW if i == 3 else ((40, 70, 120) if i < 3 else (25, 50, 95)), r=8) + text((x + 110, 802), f'Episode {i + 1}', semi(26), NAVY if i == 3 else (WHITE if i > 3 else MUTED), 'ma') +blaze((180, 820), 56, (255, 138, 61)) +text((290, 750), 'TONIGHT\'S RUN', semi(26), MUTED) +save('squeeze-back-2-marathon.png') + +# 3. A cast promo between programmes. +screen() +logo(RES / 'orbit_drive_in_epg.webp', 820, 70, 280) +text((960, 210), 'TONIGHT AT THE DRIVE-IN', anton(64), YELLOW, 'ma') +art(460, 320, 1000, 380, 'EALING COMEDY NIGHT', (40, 70, 60)) +text((960, 730), 'Two Ealing comedies, back to back', semi(40), WHITE, 'ma') +text((960, 790), 'From 19:00 · Saturday', reg(32), MUTED, 'ma') +mascot((300, 560), 90); bumble((1640, 520), 70, None) +text((960, 880), 'A promo plays in the ident\'s place now and then, never more than one an hour per channel.', reg(26), MUTED, 'ma') +motion(80, 940, 760, 110, ['Ollie rolls in from the left, Bumble buzzes', 'in from the right. Still with Reduce motion.']) +save('squeeze-back-3-promo.png') + +# 4. How it times. +screen() +text((80, 80), 'HOW THE SQUEEZE-BACK TIMES', anton(52), YELLOW) +text((80, 160), 'The programme keeps playing throughout: nothing is cut, and the next one still starts exactly on time.', reg(30), MUTED) +x0, x1, y = 300, 1620, 420 +D().line((S(x0), S(y), S(x1), S(y)), fill=(80, 100, 150), width=SW(6)) +marks = [(x0, 'Credits begin', 'Emby\'s credits marker, if the file has one,\nor the last 60 seconds'), (x0 + 440, 'Squeeze-back', 'Picture eases into its frame\nover 1 second; sound stays'), + (x0 + 880, 'Ident', 'The channel\'s ident, as now,\nfor its last seconds'), (x1, 'Next programme', 'On time, at its booked start')] +for x, title, line in marks: + circ((x, y), 14, YELLOW) + text((x, y - 90), title, semi(32), WHITE, 'ma') + for i, l in enumerate(line.split('\n')): text((x, y + 40 + i * 34), l, reg(24), MUTED, 'ma') +rules = ['Not on Tots in the wind-down or overnight: a quiet fade instead.', 'Not when the next programme is a film\'s second half or a live special.', + 'Off in Settings › Watching for anyone who prefers the full credits.', 'Kids profile: Next shows only Kids\' channels.', 'Reduce motion: the frame changes at once, no easing; the cast stays still.'] +for i, r in enumerate(rules): text((160, 620 + i * 54), '· ' + r, reg(30), WHITE) +save('squeeze-back-4-timing.png') diff --git a/design/squeeze-back/squeeze-back-1-next.png b/design/squeeze-back/squeeze-back-1-next.png new file mode 100644 index 0000000..a5a5a62 Binary files /dev/null and b/design/squeeze-back/squeeze-back-1-next.png differ diff --git a/design/squeeze-back/squeeze-back-2-marathon.png b/design/squeeze-back/squeeze-back-2-marathon.png new file mode 100644 index 0000000..9866a25 Binary files /dev/null and b/design/squeeze-back/squeeze-back-2-marathon.png differ diff --git a/design/squeeze-back/squeeze-back-3-promo.png b/design/squeeze-back/squeeze-back-3-promo.png new file mode 100644 index 0000000..00c4c2e Binary files /dev/null and b/design/squeeze-back/squeeze-back-3-promo.png differ diff --git a/design/squeeze-back/squeeze-back-4-timing.png b/design/squeeze-back/squeeze-back-4-timing.png new file mode 100644 index 0000000..eb37bf4 Binary files /dev/null and b/design/squeeze-back/squeeze-back-4-timing.png differ