- 用 pnpm 替换 npm,锁定 pnpm@10.33.0,删除 package-lock.json 相关说明 - package.json 将渲染层依赖移入 devDependencies,减小 app.asar 体积 - 新增 electron:smoke 冒烟脚本与 icon 生成脚本 - win 平台设置 AppUserModelID,修复任务栏图标归组 - 清理 tsBuildInfoFile 输出路径至 node_modules/.tmp - 更新测试断言以匹配画幅遮罩、取景器缩放与模型库外部资产行为 - README 与打包指南补充 pnpm、镜像与常见问题说明
137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
#!/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()
|