feat(assets): 디지털렌트매니저 아이콘(집+체크·스퀴클) PNG 세트 및 생성 스크립트 추가\n\n- 경로: assets/app_icon/house_check/{32..1024}.png\n- 스크립트: scripts/render_icon.py (무의존 PNG 렌더) / scripts/generate_icons.sh
Some checks failed
Flutter CI / build (push) Has been cancelled
BIN
assets/app_icon/house_check/1024.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/app_icon/house_check/128.png
Normal file
|
After Width: | Height: | Size: 985 B |
BIN
assets/app_icon/house_check/192.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/app_icon/house_check/256.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/app_icon/house_check/32.png
Normal file
|
After Width: | Height: | Size: 312 B |
BIN
assets/app_icon/house_check/48.png
Normal file
|
After Width: | Height: | Size: 439 B |
BIN
assets/app_icon/house_check/512.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/app_icon/house_check/64.png
Normal file
|
After Width: | Height: | Size: 558 B |
BIN
assets/app_icon/house_check/96.png
Normal file
|
After Width: | Height: | Size: 776 B |
10
scripts/generate_icons.sh
Executable file
@@ -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"
|
||||||
236
scripts/render_icon.py
Normal file
@@ -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 <out_dir> <sizes...>",
|
||||||
|
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()
|
||||||
|
|
||||||