Design: the credits squeeze-back and cast promos
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 2m13s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m27s

Paul, 6 November 2026, after NostalgiaTV's v0.10.51. Over a programme's
last minute (or from Emby's credits marker) the picture eases into a frame
and Next on <channel> fills the rest with a cast member, then the ident as
now; a marathon look for one-show runs; a Tonight at the Drive-In promo in
the ident's place now and then; the timing and rules. For approval only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-06 00:38:15 +00:00
co-authored by Claude Opus 5.5
parent 2a39a770e6
commit cbd6826eb4
5 changed files with 106 additions and 0 deletions
+106
View File
@@ -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 <channel>" 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')
Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB