#!/usr/bin/env python3 """ Frame Composer - Utilities for composing visual elements into frames. Provides functions for drawing shapes, text, emojis, and compositing elements together to create animation frames. """ from typing import Optional import numpy as np from PIL import Image, ImageDraw, ImageFont def create_blank_frame( width: int, height: int, color: tuple[int, int, int] = (255, 255, 255) ) -> Image.Image: """ Create a blank frame with solid color background. Args: width: Frame width height: Frame height color: RGB color tuple (default: white) Returns: PIL Image """ return Image.new("RGB", (width, height), color) def draw_circle( frame: Image.Image, center: tuple[int, int], radius: int, fill_color: Optional[tuple[int, int, int]] = None, outline_color: Optional[tuple[int, int, int]] = None, outline_width: int = 1, ) -> Image.Image: """ Draw a circle on a frame. Args: frame: PIL Image to draw on center: (x, y) center position radius: Circle radius fill_color: RGB fill color (None for no fill) outline_color: RGB outline color (None for no outline) outline_width: Outline width in pixels Returns: Modified frame """ draw = ImageDraw.Draw(frame) x, y = center bbox = [x - radius, y - radius, x + radius, y + radius] draw.ellipse(bbox, fill=fill_color, outline=outline_color, width=outline_width) return frame def draw_text( frame: Image.Image, text: str, position: tuple[int, int], color: tuple[int, int, int] = (0, 0, 0), centered: bool = False, ) -> Image.Image: """ Draw text on a frame. Args: frame: PIL Image to draw on text: Text to draw position: (x, y) position (top-left unless centered=True) color: RGB text color centered: If True, center text at position Returns: Modified frame """ draw = ImageDraw.Draw(frame) # Uses Pillow's default font. # If the font should be changed for the emoji, add additional logic here. font = ImageFont.load_default() if centered: bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] x = position[0] - text_width // 2 y = position[1] - text_height // 2 position = (x, y) draw.text(position, text, fill=color, font=font) return frame def create_gradient_background( width: int, height: int, top_color: tuple[int, int, int], bottom_color: tuple[int, int, int], ) -> Image.Image: """ Create a vertical gradient background. Args: width: Frame width height: Frame height top_color: RGB color at top bottom_color: RGB color at bottom Returns: PIL Image with gradient """ frame = Image.new("RGB", (width, height)) draw = ImageDraw.Draw(frame) # Calculate color step for each row r1, g1, b1 = top_color r2, g2, b2 = bottom_color for y in range(height): # Interpolate color ratio = y / height r = int(r1 * (1 - ratio) + r2 * ratio) g = int(g1 * (1 - ratio) + g2 * ratio) b = int(b1 * (1 - ratio) + b2 * ratio) # Draw horizontal line draw.line([(0, y), (width, y)], fill=(r, g, b)) return frame def draw_star( frame: Image.Image, center: tuple[int, int], size: int, fill_color: tuple[int, int, int], outline_color: Optional[tuple[int, int, int]] = None, outline_width: int = 1, ) -> Image.Image: """ Draw a 5-pointed star. Args: frame: PIL Image to draw on center: (x, y) center position size: Star size (outer radius) fill_color: RGB fill color outline_color: RGB outline color (None for no outline) outline_width: Outline width Returns: Modified frame """ import math draw = ImageDraw.Draw(frame) x, y = center # Calculate star points points = [] for i in range(10): angle = (i * 36 - 90) * math.pi / 180 # 36 degrees per point, start at top radius = size if i % 2 == 0 else size * 0.4 # Alternate between outer and inner px = x + radius * math.cos(angle) py = y + radius * math.sin(angle) points.append((px, py)) # Draw star draw.polygon(points, fill=fill_color, outline=outline_color, width=outline_width) return frame