main
  1#!/usr/bin/env python3
  2"""
  3Frame Composer - Utilities for composing visual elements into frames.
  4
  5Provides functions for drawing shapes, text, emojis, and compositing elements
  6together to create animation frames.
  7"""
  8
  9from typing import Optional
 10
 11import numpy as np
 12from PIL import Image, ImageDraw, ImageFont
 13
 14
 15def create_blank_frame(
 16    width: int, height: int, color: tuple[int, int, int] = (255, 255, 255)
 17) -> Image.Image:
 18    """
 19    Create a blank frame with solid color background.
 20
 21    Args:
 22        width: Frame width
 23        height: Frame height
 24        color: RGB color tuple (default: white)
 25
 26    Returns:
 27        PIL Image
 28    """
 29    return Image.new("RGB", (width, height), color)
 30
 31
 32def draw_circle(
 33    frame: Image.Image,
 34    center: tuple[int, int],
 35    radius: int,
 36    fill_color: Optional[tuple[int, int, int]] = None,
 37    outline_color: Optional[tuple[int, int, int]] = None,
 38    outline_width: int = 1,
 39) -> Image.Image:
 40    """
 41    Draw a circle on a frame.
 42
 43    Args:
 44        frame: PIL Image to draw on
 45        center: (x, y) center position
 46        radius: Circle radius
 47        fill_color: RGB fill color (None for no fill)
 48        outline_color: RGB outline color (None for no outline)
 49        outline_width: Outline width in pixels
 50
 51    Returns:
 52        Modified frame
 53    """
 54    draw = ImageDraw.Draw(frame)
 55    x, y = center
 56    bbox = [x - radius, y - radius, x + radius, y + radius]
 57    draw.ellipse(bbox, fill=fill_color, outline=outline_color, width=outline_width)
 58    return frame
 59
 60
 61def draw_text(
 62    frame: Image.Image,
 63    text: str,
 64    position: tuple[int, int],
 65    color: tuple[int, int, int] = (0, 0, 0),
 66    centered: bool = False,
 67) -> Image.Image:
 68    """
 69    Draw text on a frame.
 70
 71    Args:
 72        frame: PIL Image to draw on
 73        text: Text to draw
 74        position: (x, y) position (top-left unless centered=True)
 75        color: RGB text color
 76        centered: If True, center text at position
 77
 78    Returns:
 79        Modified frame
 80    """
 81    draw = ImageDraw.Draw(frame)
 82
 83    # Uses Pillow's default font.
 84    # If the font should be changed for the emoji, add additional logic here.
 85    font = ImageFont.load_default()
 86
 87    if centered:
 88        bbox = draw.textbbox((0, 0), text, font=font)
 89        text_width = bbox[2] - bbox[0]
 90        text_height = bbox[3] - bbox[1]
 91        x = position[0] - text_width // 2
 92        y = position[1] - text_height // 2
 93        position = (x, y)
 94
 95    draw.text(position, text, fill=color, font=font)
 96    return frame
 97
 98
 99def create_gradient_background(
100    width: int,
101    height: int,
102    top_color: tuple[int, int, int],
103    bottom_color: tuple[int, int, int],
104) -> Image.Image:
105    """
106    Create a vertical gradient background.
107
108    Args:
109        width: Frame width
110        height: Frame height
111        top_color: RGB color at top
112        bottom_color: RGB color at bottom
113
114    Returns:
115        PIL Image with gradient
116    """
117    frame = Image.new("RGB", (width, height))
118    draw = ImageDraw.Draw(frame)
119
120    # Calculate color step for each row
121    r1, g1, b1 = top_color
122    r2, g2, b2 = bottom_color
123
124    for y in range(height):
125        # Interpolate color
126        ratio = y / height
127        r = int(r1 * (1 - ratio) + r2 * ratio)
128        g = int(g1 * (1 - ratio) + g2 * ratio)
129        b = int(b1 * (1 - ratio) + b2 * ratio)
130
131        # Draw horizontal line
132        draw.line([(0, y), (width, y)], fill=(r, g, b))
133
134    return frame
135
136
137def draw_star(
138    frame: Image.Image,
139    center: tuple[int, int],
140    size: int,
141    fill_color: tuple[int, int, int],
142    outline_color: Optional[tuple[int, int, int]] = None,
143    outline_width: int = 1,
144) -> Image.Image:
145    """
146    Draw a 5-pointed star.
147
148    Args:
149        frame: PIL Image to draw on
150        center: (x, y) center position
151        size: Star size (outer radius)
152        fill_color: RGB fill color
153        outline_color: RGB outline color (None for no outline)
154        outline_width: Outline width
155
156    Returns:
157        Modified frame
158    """
159    import math
160
161    draw = ImageDraw.Draw(frame)
162    x, y = center
163
164    # Calculate star points
165    points = []
166    for i in range(10):
167        angle = (i * 36 - 90) * math.pi / 180  # 36 degrees per point, start at top
168        radius = size if i % 2 == 0 else size * 0.4  # Alternate between outer and inner
169        px = x + radius * math.cos(angle)
170        py = y + radius * math.sin(angle)
171        points.append((px, py))
172
173    # Draw star
174    draw.polygon(points, fill=fill_color, outline=outline_color, width=outline_width)
175
176    return frame