#!/usr/bin/env python3 """生成应用图标 public/icon.png(512x512)。 仓库原先引用了 public/icon.png 但文件缺失,导致 electron-builder 回落到默认 Electron 图标。本脚本零第三方依赖(仅标准库 zlib/struct),可重复生成: python scripts/generate_icon.py 图形语义:视口取景框(四角括号)+ 播放三角,对应"预演/播放分镜"。 """ from __future__ import annotations import struct import zlib from pathlib import Path SIZE = 512 SS = 3 # 超采样倍率,用于边缘抗锯齿 W = SIZE * SS # 取自 src/styles/index.css 的设计令牌 BG_TOP = (22, 24, 28) BG_BOTTOM = (9, 9, 9) BRACKET = (242, 244, 246) ACCENT = (3, 150, 255) def rounded_rect_coverage(px: float, py: float, x0: float, y0: float, x1: float, y1: float, radius: float) -> bool: if not (x0 <= px <= x1 and y0 <= py <= y1): return False cx = min(max(px, x0 + radius), x1 - radius) cy = min(max(py, y0 + radius), y1 - radius) dx = px - cx dy = py - cy return dx * dx + dy * dy <= radius * radius def bracket_coverage(px: float, py: float, inset: float, arm: float, thickness: float) -> bool: """四角取景框括号:每角由一条横臂和一条竖臂组成。""" lo = inset hi = SIZE - inset horizontal_arm = (lo <= px <= lo + arm) or (hi - arm <= px <= hi) vertical_arm = (lo <= py <= lo + arm) or (hi - arm <= py <= hi) horizontal_bar = (lo <= py <= lo + thickness) or (hi - thickness <= py <= hi) vertical_bar = (lo <= px <= lo + thickness) or (hi - thickness <= px <= hi) return (horizontal_arm and horizontal_bar) or (vertical_arm and vertical_bar) def triangle_coverage(px: float, py: float) -> bool: ax, ay = 196.0, 168.0 bx, by = 196.0, 344.0 cx, cy = 356.0, 256.0 d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by) d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy) d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay) has_neg = d1 < 0 or d2 < 0 or d3 < 0 has_pos = d1 > 0 or d2 > 0 or d3 > 0 return not (has_neg and has_pos) def build_pixels() -> bytearray: samples = SS * SS pixels = bytearray(SIZE * SIZE * 4) for oy in range(SIZE): for ox in range(SIZE): bg_hits = 0 bracket_hits = 0 triangle_hits = 0 for sy in range(SS): for sx in range(SS): px = ox + (sx + 0.5) / SS py = oy + (sy + 0.5) / SS if rounded_rect_coverage(px, py, 0.0, 0.0, float(SIZE), float(SIZE), 118.0): bg_hits += 1 if bracket_coverage(px, py, inset=92.0, arm=104.0, thickness=26.0): bracket_hits += 1 if triangle_coverage(px, py): triangle_hits += 1 alpha = bg_hits / samples if alpha <= 0: continue ratio = oy / (SIZE - 1) base = tuple( BG_TOP[channel] + (BG_BOTTOM[channel] - BG_TOP[channel]) * ratio for channel in range(3) ) accent_cov = (bracket_hits + triangle_hits) / samples if accent_cov > 0: layer = BRACKET if bracket_hits >= triangle_hits else ACCENT layer_alpha = min(1.0, accent_cov / max(alpha, 1e-6)) base = tuple( base[channel] + (layer[channel] - base[channel]) * layer_alpha for channel in range(3) ) offset = (oy * SIZE + ox) * 4 pixels[offset] = int(round(base[0])) pixels[offset + 1] = int(round(base[1])) pixels[offset + 2] = int(round(base[2])) pixels[offset + 3] = int(round(alpha * 255)) return pixels def write_png(path: Path, pixels: bytearray) -> None: raw = bytearray() stride = SIZE * 4 for y in range(SIZE): raw.append(0) # filter type 0 raw.extend(pixels[y * stride : (y + 1) * stride]) def chunk(tag: bytes, payload: bytes) -> bytes: return ( struct.pack(">I", len(payload)) + tag + payload + struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF) ) png = b"\x89PNG\r\n\x1a\n" png += chunk(b"IHDR", struct.pack(">IIBBBBB", SIZE, SIZE, 8, 6, 0, 0, 0)) png += chunk(b"IDAT", zlib.compress(bytes(raw), 9)) png += chunk(b"IEND", b"") path.write_bytes(png) def main() -> None: target = Path(__file__).resolve().parent.parent / "public" / "icon.png" target.parent.mkdir(parents=True, exist_ok=True) write_png(target, build_pixels()) print(f"wrote {target} ({target.stat().st_size} bytes, {SIZE}x{SIZE})") if __name__ == "__main__": main()