frame_composer.py 4.4 KB

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