213 lines
6.0 KiB
Python
213 lines
6.0 KiB
Python
"""Generate the app icon: olive-green rounded square with a single elegant note."""
|
|
import struct
|
|
import zlib
|
|
import math
|
|
import os
|
|
|
|
BG_R, BG_G, BG_B = 124, 154, 60
|
|
|
|
|
|
def create_icon():
|
|
sizes = [16, 32, 48, 64, 128, 256]
|
|
images = []
|
|
for s in sizes:
|
|
images.append(_render(s))
|
|
_write_ico(images, sizes,
|
|
os.path.join(os.path.dirname(__file__), "app.ico"))
|
|
print("app.ico created!")
|
|
|
|
|
|
def _render(size):
|
|
"""Render an olive-green rounded-square with a single music note."""
|
|
pixels = bytearray(size * size * 4)
|
|
corner = size * 0.22
|
|
|
|
for y in range(size):
|
|
for x in range(size):
|
|
i = (y * size + x) * 4
|
|
|
|
inside = _rounded_rect(x, y, size, corner)
|
|
|
|
if inside >= 1.0:
|
|
nr, ng, nb, na = _note_pixel(x, y, size)
|
|
if na > 0:
|
|
pixels[i] = nr
|
|
pixels[i + 1] = ng
|
|
pixels[i + 2] = nb
|
|
pixels[i + 3] = na
|
|
else:
|
|
pixels[i] = BG_R
|
|
pixels[i + 1] = BG_G
|
|
pixels[i + 2] = BG_B
|
|
pixels[i + 3] = 255
|
|
elif inside > 0:
|
|
a = int(inside * 255)
|
|
nr, ng, nb, na = _note_pixel(x, y, size)
|
|
if na > 0:
|
|
pixels[i] = nr
|
|
pixels[i + 1] = ng
|
|
pixels[i + 2] = nb
|
|
pixels[i + 3] = min(255, int(na * inside))
|
|
else:
|
|
pixels[i] = BG_R
|
|
pixels[i + 1] = BG_G
|
|
pixels[i + 2] = BG_B
|
|
pixels[i + 3] = a
|
|
else:
|
|
pixels[i:i + 4] = b'\x00\x00\x00\x00'
|
|
|
|
return bytes(pixels)
|
|
|
|
|
|
def _rounded_rect(x, y, size, corner):
|
|
"""Return 0.0-1.0 coverage for anti-aliased rounded rectangle."""
|
|
margin = size * 0.06
|
|
left, top = margin, margin
|
|
right, bottom = size - margin - 1, size - margin - 1
|
|
|
|
if x < left or x > right or y < top or y > bottom:
|
|
return 0.0
|
|
|
|
cr = corner
|
|
corners = [
|
|
(left + cr, top + cr),
|
|
(right - cr, top + cr),
|
|
(left + cr, bottom - cr),
|
|
(right - cr, bottom - cr),
|
|
]
|
|
|
|
for ccx, ccy in corners:
|
|
in_corner_x = (x < ccx and ccx == corners[0][0] or
|
|
x < ccx and ccx == corners[2][0] or
|
|
x > ccx and ccx == corners[1][0] or
|
|
x > ccx and ccx == corners[3][0])
|
|
in_corner_y = (y < ccy and ccy == corners[0][1] or
|
|
y < ccy and ccy == corners[1][1] or
|
|
y > ccy and ccy == corners[2][1] or
|
|
y > ccy and ccy == corners[3][1])
|
|
if in_corner_x and in_corner_y:
|
|
dist = ((x - ccx) ** 2 + (y - ccy) ** 2) ** 0.5
|
|
if dist > cr + 1:
|
|
return 0.0
|
|
elif dist > cr - 1:
|
|
return max(0.0, min(1.0, cr + 1 - dist))
|
|
else:
|
|
return 1.0
|
|
|
|
return 1.0
|
|
|
|
|
|
def _note_pixel(x, y, size):
|
|
"""Draw a single elegant eighth note (quaver)."""
|
|
s = size
|
|
white = (255, 255, 255, 235)
|
|
|
|
# --- Note head: tilted filled ellipse ---
|
|
hcx = s * 0.44
|
|
hcy = s * 0.71
|
|
hrx = s * 0.14
|
|
hry = s * 0.09
|
|
tilt = -0.45
|
|
|
|
ca, sa = math.cos(tilt), math.sin(tilt)
|
|
dx, dy = x - hcx, y - hcy
|
|
u = (dx * ca + dy * sa) / hrx
|
|
v = (-dx * sa + dy * ca) / hry
|
|
d2 = u * u + v * v
|
|
|
|
if d2 <= 1.0:
|
|
return white
|
|
if d2 <= 1.25:
|
|
alpha = max(0, int(235 * (1.25 - d2) / 0.25))
|
|
if alpha > 10:
|
|
return (255, 255, 255, alpha)
|
|
|
|
# --- Stem: thin vertical line from head to top ---
|
|
stem_x = hcx + hrx * ca
|
|
stem_hw = max(0.6, s * 0.018)
|
|
stem_top = s * 0.19
|
|
stem_bot = hcy - hry * 0.15
|
|
|
|
if stem_top <= y <= stem_bot and abs(x - stem_x) <= stem_hw:
|
|
edge = stem_hw - abs(x - stem_x)
|
|
if edge < 1.0:
|
|
return (255, 255, 255, max(0, int(235 * edge)))
|
|
return white
|
|
|
|
# --- Flag: elegant curved shape at top of stem ---
|
|
ft = stem_top
|
|
fh = s * 0.32
|
|
fb = ft + fh
|
|
fw = s * 0.19
|
|
|
|
if ft <= y <= fb and x >= stem_x - stem_hw:
|
|
t = (y - ft) / fh
|
|
bulge = fw * math.sin(t * math.pi * 0.82) * ((1 - t) ** 0.65)
|
|
right_edge = stem_x + stem_hw + bulge
|
|
|
|
if x <= right_edge:
|
|
edge_dist = right_edge - x
|
|
if edge_dist < 1.2 and bulge > stem_hw:
|
|
alpha = max(0, int(235 * edge_dist / 1.2))
|
|
if alpha > 10:
|
|
return (255, 255, 255, alpha)
|
|
return (0, 0, 0, 0)
|
|
return white
|
|
|
|
return (0, 0, 0, 0)
|
|
|
|
|
|
def _make_png(rgba_data, width, height):
|
|
"""Create a minimal PNG from RGBA pixel data."""
|
|
def chunk(ctype, data):
|
|
c = ctype + data
|
|
crc = struct.pack('>I', zlib.crc32(c) & 0xffffffff)
|
|
return struct.pack('>I', len(data)) + c + crc
|
|
|
|
raw = b''
|
|
for row in range(height):
|
|
raw += b'\x00'
|
|
off = row * width * 4
|
|
raw += rgba_data[off:off + width * 4]
|
|
|
|
ihdr = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0)
|
|
compressed = zlib.compress(raw)
|
|
|
|
png = b'\x89PNG\r\n\x1a\n'
|
|
png += chunk(b'IHDR', ihdr)
|
|
png += chunk(b'IDAT', compressed)
|
|
png += chunk(b'IEND', b'')
|
|
return png
|
|
|
|
|
|
def _write_ico(images, sizes, path):
|
|
"""Write a multi-size .ico file."""
|
|
count = len(sizes)
|
|
header = struct.pack('<HHH', 0, 1, count)
|
|
|
|
dir_size = 6 + count * 16
|
|
offset = dir_size
|
|
entries = []
|
|
png_datas = []
|
|
|
|
for idx, s in enumerate(sizes):
|
|
png = _make_png(images[idx], s, s)
|
|
png_datas.append(png)
|
|
w = 0 if s >= 256 else s
|
|
h = 0 if s >= 256 else s
|
|
entry = struct.pack('<BBBBHHIH',
|
|
w, h, 0, 0, 1, 32, len(png), offset)
|
|
entries.append(entry)
|
|
offset += len(png)
|
|
|
|
with open(path, 'wb') as f:
|
|
f.write(header)
|
|
for e in entries:
|
|
f.write(e)
|
|
for p in png_datas:
|
|
f.write(p)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
create_icon()
|