make_icon.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. # -*- coding: utf-8 -*-
  2. """DoTouchAI app icon generator — 3 variants, Huawei store spec.
  3. Outputs 1024x1024 / 216x216 PNG (<3MB) and WEBP (<100KB).
  4. """
  5. from PIL import Image, ImageDraw, ImageFilter
  6. import math, os
  7. OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "dotouchai_logo")
  8. os.makedirs(OUT, exist_ok=True)
  9. SS = 3 # supersample factor
  10. BASE = 1024
  11. S = BASE * SS # render size
  12. def bilinear_bg(size, c_tl, c_tr, c_bl, c_br):
  13. img = Image.new('RGB', (2, 2))
  14. img.putpixel((0, 0), c_tl); img.putpixel((1, 0), c_tr)
  15. img.putpixel((0, 1), c_bl); img.putpixel((1, 1), c_br)
  16. return img.resize((size, size), Image.BILINEAR)
  17. def solid_bg(size, color):
  18. return Image.new('RGB', (size, size), color)
  19. def vgradient(size, c_top, c_bot):
  20. img = Image.new('RGB', (1, 256))
  21. for i in range(256):
  22. t = i / 255
  23. img.putpixel((0, i), tuple(round(a + (b - a) * t) for a, b in zip(c_top, c_bot)))
  24. return img.resize((size, size), Image.BILINEAR)
  25. def radial_glow(size, cx, cy, rx, ry, color, alpha, blur):
  26. layer = Image.new('RGBA', (size, size), (0, 0, 0, 0))
  27. d = ImageDraw.Draw(layer)
  28. d.ellipse([cx - rx, cy - ry, cx + rx, cy + ry], fill=color + (alpha,))
  29. return layer.filter(ImageFilter.GaussianBlur(blur))
  30. # ---- mark geometry (in BASE units, scaled by SS) ----
  31. PX, PY = 320, 512 # ripple origin (on the stem)
  32. STEM_W = 84
  33. RADII_Y = [140, 225, 310] # concentric arcs — spacing 85 > stroke 76
  34. RX_SCALE = 1.25
  35. STROKE = 76
  36. DOT_R = 62
  37. NODE_R = 40
  38. NODE_RING_R = 56
  39. NODE_ANGLES = (-40, 0, 40) # degrees on outer arc
  40. def draw_D_ripple(mark_mask, over_mask, under_mask, ring_mask):
  41. dm = ImageDraw.Draw(mark_mask)
  42. dov = ImageDraw.Draw(over_mask)
  43. dr = ImageDraw.Draw(ring_mask)
  44. px, py = PX * SS, PY * SS
  45. sw, st = STEM_W * SS, STROKE * SS
  46. r3y = RADII_Y[-1] * SS
  47. # stem (left bar of the D)
  48. dm.rounded_rectangle([px - sw // 2, py - r3y, px + sw // 2, py + r3y],
  49. radius=sw // 2, fill=255)
  50. # concentric arcs (right bulge of the D = touch ripples)
  51. for ry in RADII_Y:
  52. rxs, rys = int(ry * RX_SCALE * SS), ry * SS
  53. dm.arc([px - rxs, py - rys, px + rxs, py + rys],
  54. start=270, end=90, fill=255, width=st)
  55. # touch point (accent dot at ripple origin)
  56. dr_ = DOT_R * SS
  57. dov.ellipse([px - dr_, py - dr_, px + dr_, py + dr_], fill=255)
  58. # aggregation nodes on the outer arc
  59. rx3, ry3 = int(RADII_Y[-1] * RX_SCALE * SS), RADII_Y[-1] * SS
  60. for ang in NODE_ANGLES:
  61. t = math.radians(ang)
  62. nx = px + rx3 * math.cos(t)
  63. ny = py + ry3 * math.sin(t)
  64. nr, rr = NODE_R * SS, NODE_RING_R * SS
  65. dr.ellipse([nx - rr, ny - rr, nx + rr, ny + rr], fill=255)
  66. dov.ellipse([nx - nr, ny - nr, nx + nr, ny + nr], fill=255)
  67. def draw_D_touch(mark_mask, over_mask, under_mask, ring_mask):
  68. """Variant C: D ring + touch emitter (dot + ripple rings) inside."""
  69. dm = ImageDraw.Draw(mark_mask)
  70. dov = ImageDraw.Draw(over_mask)
  71. dun = ImageDraw.Draw(under_mask)
  72. px, py = PX * SS, PY * SS
  73. st = 96 * SS
  74. r3y = RADII_Y[-1] * SS
  75. rx3 = int(RADII_Y[-1] * RX_SCALE * SS)
  76. cx, cy = px + 140 * SS, py # emitter center inside counter
  77. # ripple rings — drawn UNDER the D stroke, clipped at the stem
  78. for r in (130, 195):
  79. rs = r * SS
  80. dun.ellipse([cx - rs, cy - rs, cx + rs, cy + rs],
  81. outline=255, width=26 * SS)
  82. # clip: nothing left of the stem's right edge
  83. dun.rectangle([0, 0, px + st // 2, S], fill=0)
  84. # emitter dot (over everything)
  85. dr_ = 66 * SS
  86. dov.ellipse([cx - dr_, cy - dr_, cx + dr_, cy + dr_], fill=255)
  87. # D outline: stem + outer arc
  88. dm.rounded_rectangle([px - st // 2, py - r3y, px + st // 2, py + r3y],
  89. radius=st // 2, fill=255)
  90. dm.arc([px - rx3, py - r3y, px + rx3, py + r3y],
  91. start=270, end=90, fill=255, width=st)
  92. def render(name, bg, glow, mark_fill, over_fill, under_fill, draw_fn):
  93. img = bg(S).convert('RGBA')
  94. if glow:
  95. img.alpha_composite(glow(S))
  96. mark_mask = Image.new('L', (S, S), 0)
  97. over_mask = Image.new('L', (S, S), 0)
  98. under_mask = Image.new('L', (S, S), 0)
  99. ring_mask = Image.new('L', (S, S), 0)
  100. draw_fn(mark_mask, over_mask, under_mask, ring_mask)
  101. def fill_layer(fill):
  102. return fill(S) if callable(fill) else Image.new('RGBA', (S, S), fill)
  103. # 1. node separator rings (punch a clean gap in the arc)
  104. if ring_mask.getbbox():
  105. img.paste(Image.new('RGBA', (S, S), (255, 255, 255, 255)),
  106. (0, 0), ring_mask)
  107. # 2. under-accent (ripples beneath the D stroke)
  108. if under_mask.getbbox():
  109. img.paste(fill_layer(under_fill), (0, 0), under_mask)
  110. # 3. main mark
  111. img.paste(fill_layer(mark_fill), (0, 0), mark_mask)
  112. # 4. over-accent (touch point + nodes)
  113. img.paste(fill_layer(over_fill), (0, 0), over_mask)
  114. master = img.convert('RGB').resize((BASE, BASE), Image.LANCZOS)
  115. small = master.resize((216, 216), Image.LANCZOS)
  116. p1024 = os.path.join(OUT, f"DoTouchAI_icon_{name}_1024.png")
  117. p216 = os.path.join(OUT, f"DoTouchAI_icon_{name}_216.png")
  118. pwebp = os.path.join(OUT, f"DoTouchAI_icon_{name}_1024.webp")
  119. master.save(p1024, optimize=True)
  120. small.save(p216, optimize=True)
  121. master.save(pwebp, 'WEBP', quality=88, method=6)
  122. for p in (p1024, p216, pwebp):
  123. print(f"{os.path.basename(p):42s} {os.path.getsize(p)/1024:8.1f} KB")
  124. # ---------- Variant A: deep navy, white ripple-D, cyan accent ----------
  125. render(
  126. "A",
  127. bg=lambda s: bilinear_bg(s, (10, 18, 48), (16, 31, 82),
  128. (13, 27, 74), (30, 47, 110)),
  129. glow=lambda s: radial_glow(s, int(500 * SS), int(512 * SS),
  130. int(430 * SS), int(390 * SS),
  131. (46, 107, 255), 70, 130 * SS),
  132. mark_fill=(244, 248, 255, 255),
  133. over_fill=lambda s: vgradient(s, (63, 224, 255), (46, 123, 255)),
  134. under_fill=None,
  135. draw_fn=draw_D_ripple,
  136. )
  137. # ---------- Variant B: clean light, blue-violet gradient mark ----------
  138. render(
  139. "B",
  140. bg=lambda s: solid_bg(s, (247, 249, 255)),
  141. glow=lambda s: radial_glow(s, int(500 * SS), int(500 * SS),
  142. int(470 * SS), int(430 * SS),
  143. (218, 228, 255), 120, 110 * SS),
  144. mark_fill=lambda s: vgradient(s, (43, 107, 246), (124, 58, 237)),
  145. over_fill=(38, 50, 116, 255),
  146. under_fill=None,
  147. draw_fn=draw_D_ripple,
  148. )
  149. # ---------- Variant C: dark violet, D-ring with touch emitter ----------
  150. render(
  151. "C",
  152. bg=lambda s: bilinear_bg(s, (11, 16, 48), (26, 20, 78),
  153. (18, 16, 66), (42, 26, 104)),
  154. glow=lambda s: radial_glow(s, int(520 * SS), int(512 * SS),
  155. int(430 * SS), int(390 * SS),
  156. (124, 92, 255), 64, 130 * SS),
  157. mark_fill=(244, 246, 255, 255),
  158. over_fill=lambda s: vgradient(s, (56, 224, 255), (46, 123, 255)),
  159. under_fill=lambda s: vgradient(s, (157, 92, 255), (86, 150, 255)),
  160. draw_fn=draw_D_touch,
  161. )
  162. print("done ->", OUT)