HartMobile/gen_icon.py
alexanderkaptsov 39662d323a HART Mobile v1.0.1 — initial clean commit
Android app for HART protocol field devices (Bluetooth SPP / USB CP210x).
Kotlin, MVVM, Jetpack Navigation, Material Design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 23:23:18 +09:00

117 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Generate HART Mobile app icon with smooth FSK waveform."""
import math
from PIL import Image, ImageDraw
SIZES = {
"mdpi": 48,
"hdpi": 72,
"xhdpi": 96,
"xxhdpi": 144,
"xxxhdpi": 192,
}
BG_COLOR = (25, 82, 148)
WAVE_COLOR = (255, 255, 255)
CORNER_RADIUS_RATIO = 0.22
def fsk_points(s, num=2000):
"""Generate centerline points of FSK waveform on canvas size s."""
margin_x = s * 0.10
margin_y = s * 0.28
w = s - 2 * margin_x
cy = s / 2
amplitude = (s - 2 * margin_y) * 0.45
cycles_fast = 3.0
cycles_slow = 1.5
transition = cycles_fast / (cycles_fast + cycles_slow * 2)
pts = []
for i in range(num + 1):
t = i / num
x = margin_x + t * w
if t <= transition:
phase = 2 * math.pi * cycles_fast * (t / transition)
else:
local_t = (t - transition) / (1 - transition)
phase = 2 * math.pi * cycles_fast + 2 * math.pi * cycles_slow * local_t
y = cy - amplitude * math.sin(phase)
pts.append((x, y))
return pts
def thick_curve_polygon(pts, thickness):
"""Build a filled polygon representing a thick smooth curve."""
half = thickness / 2.0
upper = []
lower = []
for i in range(len(pts)):
# Compute tangent direction
if i == 0:
dx, dy = pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]
elif i == len(pts) - 1:
dx, dy = pts[-1][0] - pts[-2][0], pts[-1][1] - pts[-2][1]
else:
dx, dy = pts[i + 1][0] - pts[i - 1][0], pts[i + 1][1] - pts[i - 1][1]
length = math.sqrt(dx * dx + dy * dy)
if length < 1e-9:
nx, ny = 0, 1
else:
nx, ny = -dy / length, dx / length
x, y = pts[i]
upper.append((x + nx * half, y + ny * half))
lower.append((x - nx * half, y - ny * half))
# Polygon: upper forward + lower backward
return upper + lower[::-1]
def generate_icon(size, round_mask=False):
scale = 4
s = size * scale
img = Image.new("RGBA", (s, s), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
radius = int(s * CORNER_RADIUS_RATIO)
draw.rounded_rectangle([0, 0, s - 1, s - 1], radius=radius, fill=BG_COLOR)
pts = fsk_points(s)
thickness = s * 0.055
poly = thick_curve_polygon(pts, thickness)
draw.polygon(poly, fill=WAVE_COLOR)
# Round the endpoints with circles
for p in [pts[0], pts[-1]]:
r = thickness / 2
draw.ellipse([p[0] - r, p[1] - r, p[0] + r, p[1] + r], fill=WAVE_COLOR)
img = img.resize((size, size), Image.LANCZOS)
if round_mask:
mask = Image.new("L", (size, size), 0)
ImageDraw.Draw(mask).ellipse([0, 0, size - 1, size - 1], fill=255)
img.putalpha(mask)
return img
def main():
base = "app/src/main/res"
for density, size in SIZES.items():
icon = generate_icon(size)
icon.save(f"{base}/mipmap-{density}/ic_launcher.png")
icon_round = generate_icon(size, round_mask=True)
icon_round.save(f"{base}/mipmap-{density}/ic_launcher_round.png")
print(f" mipmap-{density}: {size}x{size}")
store = generate_icon(512)
store.save(f"{base}/mipmap-xxxhdpi/store_icon_512.png")
print(" store_icon_512.png (512x512)")
if __name__ == "__main__":
main()