177 lines
4.4 KiB
Python
Executable File
177 lines
4.4 KiB
Python
Executable File
#!/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
|