diff --git a/assets/app_icon/house_check/1024.png b/assets/app_icon/house_check/1024.png new file mode 100644 index 0000000..3aaab2c Binary files /dev/null and b/assets/app_icon/house_check/1024.png differ diff --git a/assets/app_icon/house_check/128.png b/assets/app_icon/house_check/128.png new file mode 100644 index 0000000..5d5a587 Binary files /dev/null and b/assets/app_icon/house_check/128.png differ diff --git a/assets/app_icon/house_check/192.png b/assets/app_icon/house_check/192.png new file mode 100644 index 0000000..f785963 Binary files /dev/null and b/assets/app_icon/house_check/192.png differ diff --git a/assets/app_icon/house_check/256.png b/assets/app_icon/house_check/256.png new file mode 100644 index 0000000..1ff9271 Binary files /dev/null and b/assets/app_icon/house_check/256.png differ diff --git a/assets/app_icon/house_check/32.png b/assets/app_icon/house_check/32.png new file mode 100644 index 0000000..c04842b Binary files /dev/null and b/assets/app_icon/house_check/32.png differ diff --git a/assets/app_icon/house_check/48.png b/assets/app_icon/house_check/48.png new file mode 100644 index 0000000..180a94e Binary files /dev/null and b/assets/app_icon/house_check/48.png differ diff --git a/assets/app_icon/house_check/512.png b/assets/app_icon/house_check/512.png new file mode 100644 index 0000000..786b93d Binary files /dev/null and b/assets/app_icon/house_check/512.png differ diff --git a/assets/app_icon/house_check/64.png b/assets/app_icon/house_check/64.png new file mode 100644 index 0000000..851e9ea Binary files /dev/null and b/assets/app_icon/house_check/64.png differ diff --git a/assets/app_icon/house_check/96.png b/assets/app_icon/house_check/96.png new file mode 100644 index 0000000..56702f1 Binary files /dev/null and b/assets/app_icon/house_check/96.png differ diff --git a/scripts/generate_icons.sh b/scripts/generate_icons.sh new file mode 100755 index 0000000..48b9a38 --- /dev/null +++ b/scripts/generate_icons.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUT_DIR="assets/app_icon/house_check" +SIZES=(1024 512 256 192 128 96 64 48 32) + +mkdir -p "$OUT_DIR" +python3 scripts/render_icon.py "$OUT_DIR" "${SIZES[@]}" + +echo "\n아이콘 생성 완료: $OUT_DIR" diff --git a/scripts/render_icon.py b/scripts/render_icon.py new file mode 100644 index 0000000..681631c --- /dev/null +++ b/scripts/render_icon.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +""" +Generate squircle app icons with a house + check mark glyph. + +No external dependencies. Writes PNGs using zlib and CRC via stdlib. + +Usage: + python3 scripts/render_icon.py assets/app_icon/house_check 1024 512 256 192 128 96 64 48 32 +""" +import os +import sys +import math +import struct +import zlib +from typing import List, Tuple + + +NAVY = (0x0F, 0x17, 0x2A, 255) # #0F172A +WHITE = (255, 255, 255, 255) +GREEN = (0x10, 0xB9, 0x81, 255) # #10B981 + + +def srgb_to_lin(c: float) -> float: + c = c / 255.0 + if c <= 0.04045: + return c / 12.92 + return ((c + 0.055) / 1.055) ** 2.4 + + +def lin_to_srgb(c: float) -> int: + if c <= 0.0031308: + v = 12.92 * c + else: + v = 1.055 * (c ** (1.0 / 2.4)) - 0.055 + return max(0, min(255, int(round(v * 255.0)))) + + +def over(dst: Tuple[float, float, float, float], src: Tuple[float, float, float, float]): + # dst, src in linear space RGBA (0..1) + dr, dg, db, da = dst + sr, sg, sb, sa = src + out_a = sa + da * (1.0 - sa) + if out_a == 0.0: + return (0.0, 0.0, 0.0, 0.0) + out_r = (sr * sa + dr * da * (1.0 - sa)) / out_a + out_g = (sg * sa + dg * da * (1.0 - sa)) / out_a + out_b = (sb * sa + db * da * (1.0 - sa)) / out_a + return (out_r, out_g, out_b, out_a) + + +def to_lin_rgba(color: Tuple[int, int, int, int], alpha_scale: float = 1.0): + r, g, b, a = color + return ( + srgb_to_lin(r), + srgb_to_lin(g), + srgb_to_lin(b), + (a / 255.0) * alpha_scale, + ) + + +def point_in_triangle(px, py, ax, ay, bx, by, cx, cy) -> bool: + # Barycentric technique + v0x, v0y = cx - ax, cy - ay + v1x, v1y = bx - ax, by - ay + v2x, v2y = px - ax, py - ay + dot00 = v0x * v0x + v0y * v0y + dot01 = v0x * v1x + v0y * v1y + dot02 = v0x * v2x + v0y * v2y + dot11 = v1x * v1x + v1y * v1y + dot12 = v1x * v2x + v1y * v2y + denom = dot00 * dot11 - dot01 * dot01 + if denom == 0: + return False + inv = 1.0 / denom + u = (dot11 * dot02 - dot01 * dot12) * inv + v = (dot00 * dot12 - dot01 * dot02) * inv + return (u >= 0) and (v >= 0) and (u + v <= 1) + + +def point_in_rect(px, py, x0, y0, x1, y1) -> bool: + return (x0 <= px <= x1) and (y0 <= py <= y1) + + +def dist_point_to_segment(px, py, ax, ay, bx, by) -> float: + abx, aby = bx - ax, by - ay + apx, apy = px - ax, py - ay + ab2 = abx * abx + aby * aby + if ab2 == 0: + dx, dy = px - ax, py - ay + return math.hypot(dx, dy) + t = (apx * abx + apy * aby) / ab2 + if t < 0: + t = 0 + elif t > 1: + t = 1 + hx, hy = ax + t * abx, ay + t * aby + dx, dy = px - hx, py - hy + return math.hypot(dx, dy) + + +def inside_superellipse(u: float, v: float, n: float = 4.5) -> bool: + # map [0,1] -> [-1,1] + x = 2.0 * u - 1.0 + y = 2.0 * v - 1.0 + return (abs(x) ** n + abs(y) ** n) <= 1.0 + + +def render_icon(size: int) -> bytes: + # 2x2 supersampling + samples = [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)] + + # House geometry (normalized 0..1) + roof = (0.28, 0.46, 0.50, 0.30, 0.72, 0.46) # (ax,ay,bx,by,cx,cy) + body = (0.32, 0.46, 0.68, 0.76) # (x0,y0,x1,y1) + + # Check path + chk_a = (0.33, 0.60) + chk_b = (0.46, 0.74) + chk_c = (0.72, 0.48) + thickness = 0.08 # relative to width + + lin_bg = to_lin_rgba(NAVY) + lin_white = to_lin_rgba(WHITE) + lin_green = to_lin_rgba(GREEN) + + out = bytearray(size * size * 4) + idx = 0 + for j in range(size): + for i in range(size): + # Accumulate in linear + dst = (0.0, 0.0, 0.0, 0.0) + + cov_bg = 0.0 + cov_house = 0.0 + cov_check = 0.0 + for (ox, oy) in samples: + u = (i + ox) / size + v = (j + oy) / size + + if inside_superellipse(u, v): + cov_bg += 1.0 + + hx = 0 + # house coverage (triangle or rect) + if point_in_triangle(u, v, *roof): + hx = 1 + elif point_in_rect(u, v, *body): + hx = 1 + cov_house += hx + + # check coverage by distance to segments + d1 = dist_point_to_segment(u, v, *chk_a, *chk_b) + d2 = dist_point_to_segment(u, v, *chk_b, *chk_c) + if min(d1, d2) <= (thickness * 0.5): + cov_check += 1.0 + + ss = float(len(samples)) + if cov_bg > 0.0: + dst = over(dst, (lin_bg[0], lin_bg[1], lin_bg[2], lin_bg[3] * (cov_bg / ss))) + if cov_house > 0.0: + dst = over(dst, (lin_white[0], lin_white[1], lin_white[2], lin_white[3] * (cov_house / ss))) + if cov_check > 0.0: + dst = over(dst, (lin_green[0], lin_green[1], lin_green[2], lin_green[3] * (cov_check / ss))) + + r = lin_to_srgb(dst[0]) + g = lin_to_srgb(dst[1]) + b = lin_to_srgb(dst[2]) + a = max(0, min(255, int(round(dst[3] * 255.0)))) + + out[idx + 0] = r + out[idx + 1] = g + out[idx + 2] = b + out[idx + 3] = a + idx += 4 + + return png_encode(size, size, bytes(out)) + + +def png_chunk(typ: bytes, data: bytes) -> bytes: + return struct.pack( + ">I", len(data) + ) + typ + data + struct.pack( + ">I", (zlib.crc32(typ + data) & 0xFFFFFFFF) + ) + + +def png_encode(width: int, height: int, rgba: bytes) -> bytes: + # Build raw scanlines with filter 0 + stride = width * 4 + raw = bytearray() + for y in range(height): + raw.append(0) # filter type 0 + start = y * stride + raw.extend(rgba[start : start + stride]) + comp = zlib.compress(bytes(raw), level=9) + + sig = b"\x89PNG\r\n\x1a\n" + ihdr = struct.pack( + ">IIBBBBB", + width, + height, + 8, # bit depth + 6, # color type RGBA + 0, # compression + 0, # filter + 0, # interlace + ) + out = bytearray() + out.extend(sig) + out.extend(png_chunk(b"IHDR", ihdr)) + out.extend(png_chunk(b"IDAT", comp)) + out.extend(png_chunk(b"IEND", b"")) + return bytes(out) + + +def main(): + if len(sys.argv) < 3: + print( + "Usage: python3 scripts/render_icon.py ", + file=sys.stderr, + ) + sys.exit(2) + out_dir = sys.argv[1] + sizes = [int(s) for s in sys.argv[2:]] + os.makedirs(out_dir, exist_ok=True) + for s in sizes: + data = render_icon(s) + path = os.path.join(out_dir, f"{s}.png") + with open(path, "wb") as f: + f.write(data) + print(f"wrote {path}") + + +if __name__ == "__main__": + main() +