"""Render the CUW stinger and a small scene-change demonstration. Requires Pillow and FFmpeg."""
from pathlib import Path
from PIL import Image, ImageDraw, ImageChops, ImageFilter
import math
import random
import subprocess
import tempfile
import shutil

ROOT = Path(__file__).resolve().parent
W, H, FPS, COUNT = 1920, 1080, 60, 72
FFMPEG = shutil.which('ffmpeg') or r'C:\ffmpeg\bin\ffmpeg.exe'
logo = Image.open(ROOT / 'logo.png').convert('RGBA')
logo = logo.crop(logo.getbbox())
logo.thumbnail((530, 530), Image.Resampling.LANCZOS)
random.seed(17)
particles = [(random.randrange(W), random.randrange(H), random.uniform(90, 280), random.randrange(12, 60)) for _ in range(65)]
base = Image.new('RGBA', (W, H), '#001c32')
base_draw = ImageDraw.Draw(base)
for y in range(H):
    for x in range(0, W, 4):
        glow = max(0, 1 - math.hypot((x-W/2)/1050, (y-H/2)/720)) ** 2
        base_draw.line((x,y,x+3,y), fill=(0, int(28+43*glow), int(50+68*glow)))

def ease(value):
    value = max(0, min(1, value))
    return value * value * (3 - 2 * value)

def frame(number):
    t = number / FPS
    front = -300 + 2520 * ease(t / .42)
    rear = -280 + 2500 * ease((t - .75) / .43)
    mask = Image.new('L', (W, H))
    ImageDraw.Draw(mask).polygon([(rear + 260, 0), (front + 260, 0), (front, H), (rear, H)], fill=255)
    panel = base.copy()
    draw = ImageDraw.Draw(panel)
    # Parallax facets, fine dot texture, and passing trails give the wipe depth.
    for i in range(8):
        x = i * 330 - 460 + t * (95 if i % 2 else -75)
        draw.polygon([(x+300,0),(x+445,0),(x+145,H),(x,H)], fill='#00314d' if i%2 else '#00273f')
        draw.line((x+445,0,x+145,H), fill='#0b4b69', width=2)
    for y in range(32,H,32):
        for x in range(24,W,32):
            if abs(x-W/2)>400:
                draw.ellipse((x,y,x+2,y+2),fill='#175270')
    for x,y,speed,length in particles:
        px = (x+t*speed)%W
        draw.line((px-length,y,px,y), fill='#27728c',width=2)
        draw.ellipse((px-2,y-2,px+2,y+2),fill='#90b9e2')
    # Broad luminous edge strips, with a narrow gold highlight.
    for edge in (front, rear+95):
        for width,color in [(95,'#005596'),(58,'#2482b0'),(30,'#90b9e2'),(9,'#eaf5ff')]:
            draw.polygon([(edge+260-width,0),(edge+260,0),(edge,H),(edge-width,H)],fill=color)
    for edge, color, width in [(front, '#90b9e2', 36), (front-36, '#faa634', 12), (rear+36, '#90b9e2', 28), (rear+8, '#faa634', 8)]:
        draw.polygon([(edge+260-width,0),(edge+260,0),(edge,H),(edge-width,H)], fill=color)
    opacity = ease((t - .26) / .15) * (1 - ease((t - .76) / .14))
    # A rotating broken halo frames the logo without obscuring it.
    halo = Image.new('RGBA',(W,H))
    hd = ImageDraw.Draw(halo)
    for radius,start,end,color,width in [(352,18,140,'#90b9e2',3),(352,198,320,'#90b9e2',3),(369,65,106,'#faa634',7),(369,245,286,'#faa634',7),(328,0,360,'#246889',1)]:
        angle = (t-.5)*42
        hd.arc((W/2-radius,H/2-radius,W/2+radius,H/2+radius),start+angle,end+angle,fill=color,width=width)
    for side in (-1,1):
        x = W/2 + side*430
        hd.line((x,H/2,x+side*140,H/2),fill='#90b9e2',width=2)
        hd.polygon([(x+side*155,H/2),(x+side*165,H/2-6),(x+side*175,H/2),(x+side*165,H/2+6)],fill='#faa634')
    halo.putalpha(halo.getchannel('A').point(lambda a: round(a*opacity)))
    bloom=halo.filter(ImageFilter.GaussianBlur(12))
    panel.alpha_composite(bloom)
    panel.alpha_composite(halo)
    scale = 1.17 - .17*ease((t-.26)/.27)
    mark = logo.resize((round(logo.width*scale),round(logo.height*scale)),Image.Resampling.LANCZOS)
    # A soft sheen travels across the logo during its reveal.
    shine=Image.new('RGBA',mark.size)
    sd=ImageDraw.Draw(shine)
    sx = -300 + 1200*ease((t-.38)/.38)
    sd.polygon([(sx,0),(sx+75,0),(sx-95,mark.height),(sx-170,mark.height)],fill=(220,245,255,75))
    shine=shine.filter(ImageFilter.GaussianBlur(12))
    shine.putalpha(ImageChops.multiply(shine.getchannel('A'),mark.getchannel('A')))
    mark=Image.alpha_composite(mark,shine)
    mark.putalpha(mark.getchannel('A').point(lambda a: round(a * opacity)))
    panel.alpha_composite(mark, ((W-mark.width)//2, (H-mark.height)//2))
    panel.putalpha(mask)
    return panel

with tempfile.TemporaryDirectory(prefix='cuw-stinger-') as directory:
    temp = Path(directory)
    samples = []
    for n in range(COUNT):
        overlay = frame(n)
        overlay.save(temp / f'frame-{n:03d}.png')
        if n in (0, 71):
            assert overlay.getchannel('A').getextrema() == (0, 0), 'Ends must be transparent'
        if 27 <= n <= 44:
            assert overlay.getchannel('A').getextrema() == (255, 255), 'Cut window must fully cover scene'
        background = Image.new('RGBA', (W,H), '#274652' if n < 36 else '#45375c')
        draw = ImageDraw.Draw(background)
        draw.text((W//2,H//2), 'SCENE A' if n < 36 else 'SCENE B', fill='white', font_size=80, anchor='mm')
        preview = Image.alpha_composite(background, overlay).convert('RGB').resize((960,540), Image.Resampling.LANCZOS)
        preview.save(temp / f'preview-{n:03d}.png')
        if n in (0,12,24,36,54,71):
            samples.append(preview.resize((480,270)))
    subprocess.run([FFMPEG,'-y','-v','error','-framerate',str(FPS),'-i',str(temp/'frame-%03d.png'),
        '-c:v','libvpx-vp9','-pix_fmt','yuva420p','-b:v','0','-crf','18','-deadline','good',
        '-cpu-used','4','-row-mt','1','-auto-alt-ref','0','-an',str(ROOT/'cuw-stinger.webm')],check=True)
    subprocess.run([FFMPEG,'-y','-v','error','-framerate',str(FPS),'-i',str(temp/'preview-%03d.png'),
        '-c:v','libx264','-pix_fmt','yuv420p','-crf','18','-movflags','+faststart','-an',str(ROOT/'preview.mp4')],check=True)
    sheet = Image.new('RGB',(1440,540))
    for i, sample in enumerate(samples):
        sheet.paste(sample,((i%3)*480,(i//3)*270))
    sheet.save(ROOT/'preview-frames.jpg')
print('Rendered 72 frames at 60 fps. Transparent endpoints and opaque 450–733 ms cut window verified.')
