| """ |
| Canvas Renderer - Pillow-based caption frame rendering |
| Replicates the canvas rendering logic from burned-clip-green.html |
| """ |
|
|
| from PIL import Image, ImageDraw, ImageFont, ImageFilter |
| import os |
| from typing import List, Tuple |
|
|
| |
| WIDTH = 640 |
| HEIGHT = 240 |
| GREEN_SCREEN = (0, 255, 0) |
|
|
| |
| def get_font(size: int = 52): |
| """Get the best available bold font""" |
| font_paths = [ |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", |
| "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf", |
| "/System/Library/Fonts/Helvetica.ttc", |
| "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", |
| ] |
| for path in font_paths: |
| if os.path.exists(path): |
| try: |
| return ImageFont.truetype(path, size) |
| except: |
| continue |
| |
| return ImageFont.load_default() |
|
|
|
|
| def render_frame(words: List[str], active_index: int, style: str = "hormozi") -> Image.Image: |
| """ |
| Render a single caption frame with the specified style. |
| |
| Args: |
| words: List of words to display |
| active_index: Index of the currently active (highlighted) word (-1 for none) |
| style: One of 'hormozi', 'cinematic', 'netflix', 'neon' |
| |
| Returns: |
| PIL Image with green screen background and rendered captions |
| """ |
| |
| img = Image.new('RGB', (WIDTH, HEIGHT), GREEN_SCREEN) |
| draw = ImageDraw.Draw(img) |
| |
| font = get_font(52) |
| padding = 18 |
| center_x = WIDTH // 2 |
| center_y = HEIGHT // 2 |
| |
| |
| word_metrics = [] |
| total_width = 0 |
| |
| for idx, word in enumerate(words): |
| text = word.upper() |
| bbox = draw.textbbox((0, 0), text, font=font) |
| w = bbox[2] - bbox[0] |
| word_metrics.append({ |
| 'text': text, |
| 'width': w, |
| 'is_active': idx == active_index |
| }) |
| total_width += w + padding |
| |
| total_width -= padding |
| |
| |
| current_x = center_x - (total_width // 2) |
| |
| |
| for wm in word_metrics: |
| word_center_x = current_x + (wm['width'] // 2) |
| is_active = wm['is_active'] |
| text = wm['text'] |
| |
| |
| bbox = draw.textbbox((0, 0), text, font=font) |
| text_height = bbox[3] - bbox[1] |
| text_y = center_y - (text_height // 2) |
| text_x = current_x |
| |
| if style == 'hormozi': |
| _draw_hormozi(img, draw, text, text_x, text_y, is_active, font) |
| elif style == 'cinematic': |
| _draw_cinematic(img, draw, text, text_x, text_y, is_active, font) |
| elif style == 'netflix': |
| _draw_netflix(img, draw, text, text_x, text_y, is_active, font) |
| elif style == 'neon': |
| _draw_neon(img, draw, text, text_x, text_y, is_active, font) |
| else: |
| _draw_hormozi(img, draw, text, text_x, text_y, is_active, font) |
| |
| current_x += wm['width'] + padding |
| |
| return img |
|
|
|
|
| def _draw_text_with_outline(draw, text, x, y, font, fill_color, outline_color, outline_width=3): |
| """Draw text with outline (stroke)""" |
| |
| for dx in range(-outline_width, outline_width + 1): |
| for dy in range(-outline_width, outline_width + 1): |
| if dx != 0 or dy != 0: |
| draw.text((x + dx, y + dy), text, font=font, fill=outline_color) |
| |
| draw.text((x, y), text, font=font, fill=fill_color) |
|
|
|
|
| def _draw_hormozi(img, draw, text, x, y, is_active, font): |
| """Hormozi style: Gold active, white inactive, pop effect""" |
| if is_active: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(255, 215, 0), |
| outline_color=(0, 0, 0), |
| outline_width=4) |
| else: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(255, 255, 255), |
| outline_color=(0, 0, 0), |
| outline_width=3) |
|
|
|
|
| def _draw_cinematic(img, draw, text, x, y, is_active, font): |
| """Cinematic style: Bright white active with cyan tint, dimmed inactive""" |
| if is_active: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(255, 255, 255), |
| outline_color=(0, 0, 0), |
| outline_width=5) |
| else: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(180, 180, 180), |
| outline_color=(0, 0, 0), |
| outline_width=3) |
|
|
|
|
| def _draw_netflix(img, draw, text, x, y, is_active, font): |
| """Netflix style: Red active, white inactive""" |
| if is_active: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(229, 9, 20), |
| outline_color=(0, 0, 0), |
| outline_width=5) |
| else: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(255, 255, 255), |
| outline_color=(0, 0, 0), |
| outline_width=4) |
|
|
|
|
| def _draw_neon(img, draw, text, x, y, is_active, font): |
| """Neon style: Magenta active, cyan inactive""" |
| if is_active: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(255, 0, 255), |
| outline_color=(0, 0, 0), |
| outline_width=4) |
| else: |
| |
| _draw_text_with_outline(draw, text, x, y, font, |
| fill_color=(0, 255, 255), |
| outline_color=(0, 0, 0), |
| outline_width=3) |
|
|
|
|
| |
| if __name__ == "__main__": |
| test_words = ["WATCH", "THIS", "NOW"] |
| for i in range(-1, 3): |
| img = render_frame(test_words, i, "hormozi") |
| img.save(f"test_frame_{i}.png") |
| print(f"Saved test_frame_{i}.png") |
|
|