# -*- coding: utf-8 -*- """DoTouchAI app icon generator — 3 variants, Huawei store spec. Outputs 1024x1024 / 216x216 PNG (<3MB) and WEBP (<100KB). """ from PIL import Image, ImageDraw, ImageFilter import math, os OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "dotouchai_logo") os.makedirs(OUT, exist_ok=True) SS = 3 # supersample factor BASE = 1024 S = BASE * SS # render size def bilinear_bg(size, c_tl, c_tr, c_bl, c_br): img = Image.new('RGB', (2, 2)) img.putpixel((0, 0), c_tl); img.putpixel((1, 0), c_tr) img.putpixel((0, 1), c_bl); img.putpixel((1, 1), c_br) return img.resize((size, size), Image.BILINEAR) def solid_bg(size, color): return Image.new('RGB', (size, size), color) def vgradient(size, c_top, c_bot): img = Image.new('RGB', (1, 256)) for i in range(256): t = i / 255 img.putpixel((0, i), tuple(round(a + (b - a) * t) for a, b in zip(c_top, c_bot))) return img.resize((size, size), Image.BILINEAR) def radial_glow(size, cx, cy, rx, ry, color, alpha, blur): layer = Image.new('RGBA', (size, size), (0, 0, 0, 0)) d = ImageDraw.Draw(layer) d.ellipse([cx - rx, cy - ry, cx + rx, cy + ry], fill=color + (alpha,)) return layer.filter(ImageFilter.GaussianBlur(blur)) # ---- mark geometry (in BASE units, scaled by SS) ---- PX, PY = 320, 512 # ripple origin (on the stem) STEM_W = 84 RADII_Y = [140, 225, 310] # concentric arcs — spacing 85 > stroke 76 RX_SCALE = 1.25 STROKE = 76 DOT_R = 62 NODE_R = 40 NODE_RING_R = 56 NODE_ANGLES = (-40, 0, 40) # degrees on outer arc def draw_D_ripple(mark_mask, over_mask, under_mask, ring_mask): dm = ImageDraw.Draw(mark_mask) dov = ImageDraw.Draw(over_mask) dr = ImageDraw.Draw(ring_mask) px, py = PX * SS, PY * SS sw, st = STEM_W * SS, STROKE * SS r3y = RADII_Y[-1] * SS # stem (left bar of the D) dm.rounded_rectangle([px - sw // 2, py - r3y, px + sw // 2, py + r3y], radius=sw // 2, fill=255) # concentric arcs (right bulge of the D = touch ripples) for ry in RADII_Y: rxs, rys = int(ry * RX_SCALE * SS), ry * SS dm.arc([px - rxs, py - rys, px + rxs, py + rys], start=270, end=90, fill=255, width=st) # touch point (accent dot at ripple origin) dr_ = DOT_R * SS dov.ellipse([px - dr_, py - dr_, px + dr_, py + dr_], fill=255) # aggregation nodes on the outer arc rx3, ry3 = int(RADII_Y[-1] * RX_SCALE * SS), RADII_Y[-1] * SS for ang in NODE_ANGLES: t = math.radians(ang) nx = px + rx3 * math.cos(t) ny = py + ry3 * math.sin(t) nr, rr = NODE_R * SS, NODE_RING_R * SS dr.ellipse([nx - rr, ny - rr, nx + rr, ny + rr], fill=255) dov.ellipse([nx - nr, ny - nr, nx + nr, ny + nr], fill=255) def draw_D_touch(mark_mask, over_mask, under_mask, ring_mask): """Variant C: D ring + touch emitter (dot + ripple rings) inside.""" dm = ImageDraw.Draw(mark_mask) dov = ImageDraw.Draw(over_mask) dun = ImageDraw.Draw(under_mask) px, py = PX * SS, PY * SS st = 96 * SS r3y = RADII_Y[-1] * SS rx3 = int(RADII_Y[-1] * RX_SCALE * SS) cx, cy = px + 140 * SS, py # emitter center inside counter # ripple rings — drawn UNDER the D stroke, clipped at the stem for r in (130, 195): rs = r * SS dun.ellipse([cx - rs, cy - rs, cx + rs, cy + rs], outline=255, width=26 * SS) # clip: nothing left of the stem's right edge dun.rectangle([0, 0, px + st // 2, S], fill=0) # emitter dot (over everything) dr_ = 66 * SS dov.ellipse([cx - dr_, cy - dr_, cx + dr_, cy + dr_], fill=255) # D outline: stem + outer arc dm.rounded_rectangle([px - st // 2, py - r3y, px + st // 2, py + r3y], radius=st // 2, fill=255) dm.arc([px - rx3, py - r3y, px + rx3, py + r3y], start=270, end=90, fill=255, width=st) def render(name, bg, glow, mark_fill, over_fill, under_fill, draw_fn): img = bg(S).convert('RGBA') if glow: img.alpha_composite(glow(S)) mark_mask = Image.new('L', (S, S), 0) over_mask = Image.new('L', (S, S), 0) under_mask = Image.new('L', (S, S), 0) ring_mask = Image.new('L', (S, S), 0) draw_fn(mark_mask, over_mask, under_mask, ring_mask) def fill_layer(fill): return fill(S) if callable(fill) else Image.new('RGBA', (S, S), fill) # 1. node separator rings (punch a clean gap in the arc) if ring_mask.getbbox(): img.paste(Image.new('RGBA', (S, S), (255, 255, 255, 255)), (0, 0), ring_mask) # 2. under-accent (ripples beneath the D stroke) if under_mask.getbbox(): img.paste(fill_layer(under_fill), (0, 0), under_mask) # 3. main mark img.paste(fill_layer(mark_fill), (0, 0), mark_mask) # 4. over-accent (touch point + nodes) img.paste(fill_layer(over_fill), (0, 0), over_mask) master = img.convert('RGB').resize((BASE, BASE), Image.LANCZOS) small = master.resize((216, 216), Image.LANCZOS) p1024 = os.path.join(OUT, f"DoTouchAI_icon_{name}_1024.png") p216 = os.path.join(OUT, f"DoTouchAI_icon_{name}_216.png") pwebp = os.path.join(OUT, f"DoTouchAI_icon_{name}_1024.webp") master.save(p1024, optimize=True) small.save(p216, optimize=True) master.save(pwebp, 'WEBP', quality=88, method=6) for p in (p1024, p216, pwebp): print(f"{os.path.basename(p):42s} {os.path.getsize(p)/1024:8.1f} KB") # ---------- Variant A: deep navy, white ripple-D, cyan accent ---------- render( "A", bg=lambda s: bilinear_bg(s, (10, 18, 48), (16, 31, 82), (13, 27, 74), (30, 47, 110)), glow=lambda s: radial_glow(s, int(500 * SS), int(512 * SS), int(430 * SS), int(390 * SS), (46, 107, 255), 70, 130 * SS), mark_fill=(244, 248, 255, 255), over_fill=lambda s: vgradient(s, (63, 224, 255), (46, 123, 255)), under_fill=None, draw_fn=draw_D_ripple, ) # ---------- Variant B: clean light, blue-violet gradient mark ---------- render( "B", bg=lambda s: solid_bg(s, (247, 249, 255)), glow=lambda s: radial_glow(s, int(500 * SS), int(500 * SS), int(470 * SS), int(430 * SS), (218, 228, 255), 120, 110 * SS), mark_fill=lambda s: vgradient(s, (43, 107, 246), (124, 58, 237)), over_fill=(38, 50, 116, 255), under_fill=None, draw_fn=draw_D_ripple, ) # ---------- Variant C: dark violet, D-ring with touch emitter ---------- render( "C", bg=lambda s: bilinear_bg(s, (11, 16, 48), (26, 20, 78), (18, 16, 66), (42, 26, 104)), glow=lambda s: radial_glow(s, int(520 * SS), int(512 * SS), int(430 * SS), int(390 * SS), (124, 92, 255), 64, 130 * SS), mark_fill=(244, 246, 255, 255), over_fill=lambda s: vgradient(s, (56, 224, 255), (46, 123, 255)), under_fill=lambda s: vgradient(s, (157, 92, 255), (86, 150, 255)), draw_fn=draw_D_touch, ) print("done ->", OUT)