#!/usr/bin/env python3 """Generate deterministic 320x240 frames for LCD boundary testing.""" from __future__ import annotations import math from pathlib import Path from PIL import Image, ImageDraw, ImageFont PROJECT_ROOT = Path(__file__).resolve().parent.parent SOURCE_DIR = PROJECT_ROOT / "components" / "platform" / "assets" / "lcd_source" PREVIEW_PATH = PROJECT_ROOT / "docs" / "lcd-boundary-test-preview.gif" WIDTH = 320 HEIGHT = 240 FRAME_COUNT = 16 FRAME_PREFIX = "lcdtest" BLACK = (0, 0, 0) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) CYAN = (0, 255, 255) MAGENTA = (255, 0, 255) YELLOW = (255, 255, 0) GRAY = (132, 130, 132) DARK_GRAY = (33, 32, 33) def draw_corner_targets(draw: ImageDraw.ImageDraw) -> None: targets = ( (0, 0, RED), (WIDTH - 12, 0, GREEN), (0, HEIGHT - 12, BLUE), (WIDTH - 12, HEIGHT - 12, YELLOW), ) for x, y, color in targets: draw.rectangle((x, y, x + 11, y + 11), fill=color, outline=WHITE) def draw_color_bars(draw: ImageDraw.ImageDraw) -> None: colors = (WHITE, YELLOW, CYAN, GREEN, MAGENTA, RED, BLUE, BLACK) for index, color in enumerate(colors): x0 = index * (WIDTH // len(colors)) x1 = (index + 1) * (WIDTH // len(colors)) - 1 draw.rectangle((x0, 1, x1, 30), fill=color) def draw_motion_field(draw: ImageDraw.ImageDraw, frame_index: int) -> None: field_top = 32 field_bottom = 207 stripe_width = 16 phase = frame_index * 4 draw.rectangle((1, field_top, WIDTH - 2, field_bottom), fill=DARK_GRAY) for x in range(-HEIGHT, WIDTH + HEIGHT, stripe_width * 2): shifted = x + phase draw.polygon( ( (shifted, field_top), (shifted + stripe_width, field_top), (shifted - 160 + stripe_width, field_bottom), (shifted - 160, field_bottom), ), fill=GRAY, ) vertical_x = frame_index * (WIDTH // FRAME_COUNT) horizontal_y = field_top + frame_index * ((field_bottom - field_top + 1) // FRAME_COUNT) draw.rectangle((vertical_x, field_top, vertical_x + 2, field_bottom), fill=WHITE) draw.rectangle((1, horizontal_y, WIDTH - 2, horizontal_y + 2), fill=YELLOW) center_x = WIDTH // 2 center_y = (field_top + field_bottom) // 2 angle = (2.0 * math.pi * frame_index / FRAME_COUNT) - (math.pi / 2.0) pointer_x = round(center_x + math.cos(angle) * 62) pointer_y = round(center_y + math.sin(angle) * 62) draw.ellipse((center_x - 65, center_y - 65, center_x + 65, center_y + 65), outline=CYAN, width=2) draw.line((center_x, center_y, pointer_x, pointer_y), fill=RED, width=4) draw.ellipse((center_x - 6, center_y - 6, center_x + 6, center_y + 6), fill=WHITE, outline=BLACK) moving_x = round(center_x + math.cos(angle) * 92) moving_y = round(center_y + math.sin(angle) * 50) draw.rectangle((moving_x - 7, moving_y - 7, moving_x + 7, moving_y + 7), fill=MAGENTA, outline=WHITE) def draw_frame_footer(draw: ImageDraw.ImageDraw, frame_index: int, font: ImageFont.ImageFont) -> None: footer_top = 209 draw.rectangle((1, footer_top, WIDTH - 2, HEIGHT - 2), fill=BLACK) draw.text((16, 211), f"F{frame_index:02d}", font=font, fill=WHITE) cell_width = 12 cell_gap = 2 strip_x = 102 strip_y = 214 for index in range(FRAME_COUNT): x0 = strip_x + index * (cell_width + cell_gap) color = WHITE if index == frame_index else DARK_GRAY draw.rectangle((x0, strip_y, x0 + cell_width - 1, strip_y + 10), fill=color, outline=GRAY) for bit in range(4): x0 = strip_x + bit * 20 color = GREEN if frame_index & (1 << bit) else RED draw.rectangle((x0, 229, x0 + 15, 237), fill=color) def generate_frame(frame_index: int, font: ImageFont.ImageFont) -> Image.Image: image = Image.new("RGB", (WIDTH, HEIGHT), BLACK) draw = ImageDraw.Draw(image) draw_color_bars(draw) draw_motion_field(draw, frame_index) draw_frame_footer(draw, frame_index, font) draw.rectangle((0, 0, WIDTH - 1, HEIGHT - 1), outline=WHITE) draw.rectangle((1, 1, WIDTH - 2, HEIGHT - 2), outline=RED) draw_corner_targets(draw) return image def main() -> None: SOURCE_DIR.mkdir(parents=True, exist_ok=True) PREVIEW_PATH.parent.mkdir(parents=True, exist_ok=True) font = ImageFont.load_default(size=22) frames = [generate_frame(index, font) for index in range(FRAME_COUNT)] for index, frame in enumerate(frames): output_path = SOURCE_DIR / f"{FRAME_PREFIX}{index:02d}.png" frame.save(output_path, format="PNG", optimize=True, compress_level=9) frames[0].save( PREVIEW_PATH, format="GIF", save_all=True, append_images=frames[1:], duration=80, loop=0, disposal=2, ) print(f"Generated {FRAME_COUNT} LCD boundary frames and {PREVIEW_PATH}") if __name__ == "__main__": main()