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