返回 DeepSeek-Reasonix
figures.py
1 """Original stylised adult figure builders (flat-illustration look).
2
3 Every figure is drawn procedurally from bezier primitives — no reference
4 imagery, no likeness of real persons. All characters are fictional adults.
5 Coordinates use a local frame: face centre (cx, cy), face half-width fw.
6 """
7 from __future__ import annotations
8
9 import math
10
11 from PIL import ImageDraw, ImageFilter
12
13 from artkit import (
14 comp,
15 cubic,
16 draw_poly,
17 ellipse_poly,
18 mix,
19 new_layer,
20 rgba,
21 smooth_path,
22 soft_fill,
23 superellipse_poly,
24 )
25
26
27 def _face_outline(cx, cy, fw, fh, turn=0.0):
28 """Near-frontal face with a soft jaw; turn>0 shifts chin slightly left."""
29 chin = (cx - turn * 0.10 * fw, cy + fh)
30 segs = [
31 # left temple -> crown -> right temple
32 ((cx - 0.94 * fw, cy - 0.38 * fh), (cx - 1.02 * fw, cy - 1.02 * fh), (cx + 0.55 * fw, cy - 1.12 * fh), (cx + 0.92 * fw, cy - 0.42 * fh)),
33 # right cheek -> right jaw
34 ((cx + 0.92 * fw, cy - 0.42 * fh), (cx + 0.98 * fw, cy + 0.18 * fh), (cx + 0.62 * fw, cy + 0.62 * fh), (cx + 0.30 * fw, cy + 0.86 * fh)),
35 # right jaw -> chin -> left jaw
36 ((cx + 0.30 * fw, cy + 0.86 * fh), (cx + 0.14 * fw, cy + 1.00 * fh), (chin[0] + 0.16 * fw, chin[1] + 0.02 * fh), chin),
37 (chin, (chin[0] - 0.34 * fw, chin[1] - 0.02 * fh), (cx - 0.72 * fw, cy + 0.66 * fh), (cx - 0.88 * fw, cy + 0.30 * fh)),
38 # left cheek back to temple
39 ((cx - 0.88 * fw, cy + 0.30 * fh), (cx - 0.96 * fw, cy + 0.02 * fh), (cx - 0.97 * fw, cy - 0.16 * fh), (cx - 0.94 * fw, cy - 0.38 * fh)),
40 ]
41 return smooth_path(segs)
42
43
44 def _neck_pts(cx, cy, fw, fh):
45 top_y = cy + 0.62 * fh
46 bot_y = cy + 1.55 * fh
47 return smooth_path([
48 ((cx - 0.34 * fw, top_y), (cx - 0.30 * fw, bot_y - 0.2 * fh), (cx - 0.38 * fw, bot_y), (cx - 0.42 * fw, bot_y)),
49 ((cx - 0.42 * fw, bot_y), (cx - 0.1 * fw, bot_y + 0.06 * fh), (cx + 0.2 * fw, bot_y + 0.06 * fh), (cx + 0.34 * fw, bot_y)),
50 ((cx + 0.34 * fw, bot_y), (cx + 0.28 * fw, bot_y - 0.2 * fh), (cx + 0.30 * fw, top_y), (cx + 0.30 * fw, top_y)),
51 ])
52
53
54 def _shoulders_pts(cx, y0, width, height, slope=0.12):
55 hw = width / 2
56 return smooth_path([
57 ((cx - hw, y0 + height), (cx - hw * 1.02, y0 + height * 0.35), (cx - hw * 0.72, y0 + slope * height), (cx - hw * 0.30, y0 + 0.02 * height)),
58 ((cx - hw * 0.30, y0 + 0.02 * height), (cx - hw * 0.1, y0 - 0.06 * height), (cx + hw * 0.1, y0 - 0.06 * height), (cx + hw * 0.30, y0 + 0.02 * height)),
59 ((cx + hw * 0.30, y0 + 0.02 * height), (cx + hw * 0.72, y0 + slope * height), (cx + hw * 1.02, y0 + height * 0.35), (cx + hw, y0 + height)),
60 ((cx + hw, y0 + height), (cx + hw * 0.4, y0 + height * 1.04), (cx - hw * 0.4, y0 + height * 1.04), (cx - hw, y0 + height)),
61 ])
62
63
64 def _closed_eye(d, ex, ey, s, color, alpha=235, lashes=0, w=0.16):
65 pts = cubic((ex - 0.16 * s, ey), (ex - 0.05 * s, ey + 0.10 * s), (ex + 0.06 * s, ey + 0.10 * s), (ex + 0.17 * s, ey - 0.01 * s), n=24)
66 d.line(pts, fill=rgba(color, alpha), width=max(2, int(w * s * 0.28)), joint="curve")
67 for i in range(lashes):
68 t = 0.55 + 0.22 * i
69 bx = ex - 0.16 * s + (0.33 * s) * t
70 by = ey + 0.06 * s - 0.05 * s * abs(t - 0.5)
71 d.line([(bx, by), (bx + 0.05 * s, by - 0.07 * s)], fill=rgba(color, alpha), width=max(2, int(w * s * 0.2)))
72
73
74 def _open_eye(lay, ex, ey, s, iris, alpha=255, sparkle=True):
75 """Simple bright anime-style eye."""
76 d = ImageDraw.Draw(lay)
77 white = (255, 255, 255)
78 d.ellipse([ex - 0.13 * s, ey - 0.17 * s, ex + 0.13 * s, ey + 0.17 * s], fill=rgba(white, alpha))
79 d.ellipse([ex - 0.10 * s, ey - 0.13 * s, ex + 0.10 * s, ey + 0.15 * s], fill=rgba(iris, alpha))
80 d.ellipse([ex - 0.055 * s, ey - 0.06 * s, ex + 0.055 * s, ey + 0.09 * s], fill=rgba(mix(iris, "#000000", 0.55), alpha))
81 if sparkle:
82 d.ellipse([ex - 0.055 * s, ey - 0.10 * s, ex + 0.005 * s, ey - 0.04 * s], fill=rgba(white, alpha))
83 d.ellipse([ex + 0.03 * s, ey + 0.02 * s, ex + 0.07 * s, ey + 0.06 * s], fill=rgba(white, int(alpha * 0.8)))
84
85
86 def _brow(d, ex, ey, s, color, alpha=200, tilt=0.0):
87 pts = cubic((ex - 0.15 * s, ey + tilt), (ex - 0.05 * s, ey - 0.06 * s + tilt), (ex + 0.08 * s, ey - 0.06 * s - tilt), (ex + 0.16 * s, ey - 0.02 * s - tilt), n=20)
88 d.line(pts, fill=rgba(color, alpha), width=max(2, int(0.035 * s)), joint="curve")
89
90
91 def _smile(d, cx, y, s, color, alpha=220, width=0.16):
92 pts = cubic((cx - width * s, y), (cx - 0.05 * s, y + 0.07 * s), (cx + 0.05 * s, y + 0.07 * s), (cx + width * s, y - 0.01 * s), n=24)
93 d.line(pts, fill=rgba(color, alpha), width=max(2, int(0.032 * s)), joint="curve")
94
95
96 def _lips(lay, cx, y, s, color, alpha=230):
97 d = ImageDraw.Draw(lay)
98 pts = smooth_path([
99 ((cx - 0.15 * s, y), (cx - 0.05 * s, y - 0.045 * s), (cx - 0.02 * s, y - 0.03 * s), (cx, y - 0.012 * s)),
100 ((cx, y - 0.012 * s), (cx + 0.02 * s, y - 0.03 * s), (cx + 0.05 * s, y - 0.045 * s), (cx + 0.15 * s, y)),
101 ((cx + 0.15 * s, y), (cx + 0.07 * s, y + 0.075 * s), (cx - 0.07 * s, y + 0.075 * s), (cx - 0.15 * s, y)),
102 ])
103 d.polygon(pts, fill=rgba(color, alpha))
104
105
106 def _blush(lay, ex, ey, s, color, alpha=70):
107 d = ImageDraw.Draw(lay)
108 d.ellipse([ex - 0.16 * s, ey - 0.08 * s, ex + 0.16 * s, ey + 0.08 * s], fill=rgba(color, alpha))
109
110
111 def _figure_shadow(base, cx, cy, fw, fh, scale_w=3.9, scale_h=4.1, alpha=26, blur=52, dx=22, dy=34):
112 lay = new_layer()
113 d = ImageDraw.Draw(lay)
114 d.ellipse(
115 [cx - fw * scale_w / 2 + dx, cy - fh * 0.9 + dy, cx + fw * scale_w / 2 + dx, cy + fh * scale_h / 2 + dy],
116 fill=(30, 24, 28, alpha),
117 )
118 comp(base, lay, blur)
119
120
121 # ---------------------------------------------------------------------------
122 # bust variants
123
124
125 def woman_serene(base, cx, cy, s, skin, hair_c, cloth_c, lip_c, blush_c, style="long", accent=None):
126 """Adult woman, gentle closed-eye expression. s = face half-width."""
127 fw, fh = s, s * 1.22
128 accent = accent or cloth_c
129 _figure_shadow(base, cx, cy, fw, fh)
130
131 # back hair mass
132 lay = new_layer()
133 d = ImageDraw.Draw(lay)
134 if style == "long":
135 back = smooth_path([
136 ((cx - 1.05 * fw, cy - 0.5 * fh), (cx - 1.25 * fw, cy - 1.5 * fh), (cx + 0.9 * fw, cy - 1.65 * fh), (cx + 1.15 * fw, cy - 0.45 * fh)),
137 ((cx + 1.15 * fw, cy - 0.45 * fh), (cx + 1.5 * fw, cy + 0.9 * fh), (cx + 1.35 * fw, cy + 2.6 * fh), (cx + 0.75 * fw, cy + 3.1 * fh)),
138 ((cx + 0.75 * fw, cy + 3.1 * fh), (cx + 0.45 * fw, cy + 2.2 * fh), (cx + 0.15 * fw, cy + 3.4 * fh), (cx - 0.35 * fw, cy + 3.5 * fh)),
139 ((cx - 0.35 * fw, cy + 3.5 * fh), (cx - 1.05 * fw, cy + 2.4 * fh), (cx - 1.35 * fw, cy + 1.0 * fh), (cx - 1.05 * fw, cy - 0.5 * fh)),
140 ])
141 else: # bun
142 back = smooth_path([
143 ((cx - 1.02 * fw, cy - 0.45 * fh), (cx - 1.1 * fw, cy - 1.4 * fh), (cx + 0.8 * fw, cy - 1.5 * fh), (cx + 1.05 * fw, cy - 0.4 * fh)),
144 ((cx + 1.05 * fw, cy - 0.4 * fh), (cx + 1.15 * fw, cy + 0.5 * fh), (cx + 0.6 * fw, cy + 1.0 * fh), (cx, cy + 1.05 * fh)),
145 ((cx, cy + 1.05 * fh), (cx - 0.7 * fw, cy + 1.0 * fh), (cx - 1.1 * fw, cy + 0.4 * fh), (cx - 1.02 * fw, cy - 0.45 * fh)),
146 ])
147 d.ellipse([cx + 0.35 * fw, cy - 1.95 * fh, cx + 1.05 * fw, cy - 1.15 * fh], fill=rgba(hair_c, 255))
148 d.polygon(back, fill=rgba(hair_c, 255))
149 base.alpha_composite(lay)
150
151 # neck + shoulders
152 lay = new_layer()
153 d = ImageDraw.Draw(lay)
154 d.polygon(_neck_pts(cx, cy, fw, fh), fill=rgba(mix(skin, "#000000", 0.07), 255))
155 sh = _shoulders_pts(cx, cy + 1.45 * fh, 3.4 * fw, 2.2 * fh)
156 d.polygon(sh, fill=rgba(cloth_c, 255))
157 # collar V
158 d.polygon(
159 [(cx - 0.30 * fw, cy + 1.50 * fh), (cx, cy + 1.95 * fh), (cx + 0.30 * fw, cy + 1.50 * fh), (cx + 0.22 * fw, cy + 1.42 * fh), (cx - 0.22 * fw, cy + 1.42 * fh)],
160 fill=rgba(mix(cloth_c, "#ffffff", 0.35), 255),
161 )
162 base.alpha_composite(lay)
163
164 # face
165 lay = new_layer()
166 d = ImageDraw.Draw(lay)
167 d.polygon(_face_outline(cx, cy, fw, fh), fill=rgba(skin, 255))
168 base.alpha_composite(lay)
169
170 # fringe / front hair
171 lay = new_layer()
172 d = ImageDraw.Draw(lay)
173 fringe = smooth_path([
174 ((cx - 1.0 * fw, cy - 0.18 * fh), (cx - 1.10 * fw, cy - 1.25 * fh), (cx + 0.6 * fw, cy - 1.48 * fh), (cx + 0.98 * fw, cy - 0.42 * fh)),
175 ((cx + 0.98 * fw, cy - 0.42 * fh), (cx + 0.72 * fw, cy - 0.58 * fh), (cx + 0.55 * fw, cy - 0.70 * fh), (cx + 0.30 * fw, cy - 0.62 * fh)),
176 ((cx + 0.30 * fw, cy - 0.62 * fh), (cx + 0.05 * fw, cy - 0.80 * fh), (cx - 0.35 * fw, cy - 0.78 * fh), (cx - 0.55 * fw, cy - 0.56 * fh)),
177 ((cx - 0.55 * fw, cy - 0.56 * fh), (cx - 0.75 * fw, cy - 0.70 * fh), (cx - 0.9 * fw, cy - 0.48 * fh), (cx - 1.0 * fw, cy - 0.18 * fh)),
178 ])
179 d.polygon(fringe, fill=rgba(hair_c, 255))
180 # side strand
181 strand = smooth_path([
182 ((cx + 0.82 * fw, cy - 0.62 * fh), (cx + 1.10 * fw, cy - 0.05 * fh), (cx + 1.05 * fw, cy + 0.9 * fh), (cx + 0.8 * fw, cy + 1.6 * fh)),
183 ((cx + 0.8 * fw, cy + 1.6 * fh), (cx + 0.66 * fw, cy + 0.8 * fh), (cx + 0.64 * fw, cy - 0.05 * fh), (cx + 0.68 * fw, cy - 0.58 * fh)),
184 ])
185 d.polygon(strand, fill=rgba(mix(hair_c, "#ffffff", 0.06), 255))
186 base.alpha_composite(lay)
187
188 # features
189 lay = new_layer()
190 d = ImageDraw.Draw(lay)
191 ink = mix(skin, "#000000", 0.72)
192 _closed_eye(d, cx - 0.42 * fw, cy - 0.02 * fh, fw, ink, lashes=2)
193 _closed_eye(d, cx + 0.42 * fw, cy - 0.02 * fh, fw, ink, lashes=2)
194 _brow(d, cx - 0.42 * fw, cy - 0.30 * fh, fw, mix(hair_c, "#000000", 0.25))
195 _brow(d, cx + 0.42 * fw, cy - 0.30 * fh, fw, mix(hair_c, "#000000", 0.25))
196 nose = cubic((cx - 0.02 * fw, cy + 0.16 * fh), (cx + 0.03 * fw, cy + 0.24 * fh), (cx + 0.02 * fw, cy + 0.30 * fh), (cx - 0.04 * fw, cy + 0.32 * fh), n=16)
197 d.line(nose, fill=rgba(mix(skin, "#000000", 0.30), 200), width=max(2, int(0.03 * fw)), joint="curve")
198 _lips(lay, cx + 0.02 * fw, cy + 0.52 * fh, fw, lip_c)
199 _blush(lay, cx - 0.52 * fw, cy + 0.30 * fh, fw, blush_c)
200 _blush(lay, cx + 0.56 * fw, cy + 0.30 * fh, fw, blush_c)
201 base.alpha_composite(lay)
202
203
204 def man_bust(base, cx, cy, s, skin, hair_c, cloth_c, collar_c=None, style="shirt"):
205 """Adult man, calm closed-eye expression. style: shirt | suit."""
206 fw, fh = s, s * 1.20
207 collar_c = collar_c or mix(cloth_c, "#ffffff", 0.5)
208 _figure_shadow(base, cx, cy, fw, fh, alpha=40)
209
210 lay = new_layer()
211 d = ImageDraw.Draw(lay)
212 d.polygon(_neck_pts(cx, cy, fw, fh), fill=rgba(mix(skin, "#000000", 0.08), 255))
213 sh = _shoulders_pts(cx, cy + 1.45 * fh, 3.6 * fw, 2.25 * fh)
214 d.polygon(sh, fill=rgba(cloth_c, 255))
215 if style == "suit":
216 # shirt V + lapels + tie
217 d.polygon([(cx - 0.38 * fw, cy + 1.5 * fh), (cx, cy + 2.35 * fh), (cx + 0.38 * fw, cy + 1.5 * fh)], fill=rgba(collar_c, 255))
218 d.polygon([(cx - 0.09 * fw, cy + 1.62 * fh), (cx + 0.09 * fw, cy + 1.62 * fh), (cx + 0.05 * fw, cy + 1.78 * fh), (cx - 0.05 * fw, cy + 1.78 * fh)], fill=rgba(mix(cloth_c, "#000000", 0.4), 255))
219 d.polygon([(cx - 0.05 * fw, cy + 1.78 * fh), (cx + 0.05 * fw, cy + 1.78 * fh), (cx + 0.10 * fw, cy + 2.35 * fh), (cx, cy + 2.5 * fh), (cx - 0.10 * fw, cy + 2.35 * fh)], fill=rgba(mix(cloth_c, "#000000", 0.4), 255))
220 lap_l = [(cx - 0.38 * fw, cy + 1.5 * fh), (cx - 0.95 * fw, cy + 1.75 * fh), (cx - 0.55 * fw, cy + 2.6 * fh), (cx - 0.12 * fw, cy + 2.1 * fh)]
221 lap_r = [(cx + 0.38 * fw, cy + 1.5 * fh), (cx + 0.95 * fw, cy + 1.75 * fh), (cx + 0.55 * fw, cy + 2.6 * fh), (cx + 0.12 * fw, cy + 2.1 * fh)]
222 d.polygon(lap_l, fill=rgba(mix(cloth_c, "#ffffff", 0.08), 255))
223 d.polygon(lap_r, fill=rgba(mix(cloth_c, "#ffffff", 0.08), 255))
224 else:
225 # crew / open collar
226 d.polygon([(cx - 0.40 * fw, cy + 1.48 * fh), (cx, cy + 1.95 * fh), (cx + 0.40 * fw, cy + 1.48 * fh), (cx + 0.28 * fw, cy + 1.38 * fh), (cx - 0.28 * fw, cy + 1.38 * fh)], fill=rgba(collar_c, 255))
227 base.alpha_composite(lay)
228
229 lay = new_layer()
230 d = ImageDraw.Draw(lay)
231 d.polygon(_face_outline(cx, cy, fw, fh, turn=0.4), fill=rgba(skin, 255))
232 base.alpha_composite(lay)
233
234 # short hair
235 lay = new_layer()
236 d = ImageDraw.Draw(lay)
237 hair = smooth_path([
238 ((cx - 0.98 * fw, cy - 0.30 * fh), (cx - 1.05 * fw, cy - 1.18 * fh), (cx + 0.6 * fw, cy - 1.38 * fh), (cx + 0.97 * fw, cy - 0.42 * fh)),
239 ((cx + 0.97 * fw, cy - 0.42 * fh), (cx + 0.86 * fw, cy - 0.60 * fh), (cx + 0.68 * fw, cy - 0.74 * fh), (cx + 0.42 * fw, cy - 0.68 * fh)),
240 ((cx + 0.42 * fw, cy - 0.68 * fh), (cx + 0.08 * fw, cy - 0.88 * fh), (cx - 0.45 * fw, cy - 0.84 * fh), (cx - 0.68 * fw, cy - 0.58 * fh)),
241 ((cx - 0.68 * fw, cy - 0.58 * fh), (cx - 0.86 * fw, cy - 0.68 * fh), (cx - 0.96 * fw, cy - 0.50 * fh), (cx - 0.98 * fw, cy - 0.30 * fh)),
242 ])
243 d.polygon(hair, fill=rgba(hair_c, 255))
244 base.alpha_composite(lay)
245
246 lay = new_layer()
247 d = ImageDraw.Draw(lay)
248 ink = mix(skin, "#000000", 0.75)
249 _closed_eye(d, cx - 0.42 * fw, cy + 0.0 * fh, fw, ink, lashes=0)
250 _closed_eye(d, cx + 0.42 * fw, cy + 0.0 * fh, fw, ink, lashes=0)
251 _brow(d, cx - 0.42 * fw, cy - 0.28 * fh, fw, mix(hair_c, "#000000", 0.2), tilt=0.01 * fw)
252 _brow(d, cx + 0.42 * fw, cy - 0.28 * fh, fw, mix(hair_c, "#000000", 0.2), tilt=0.01 * fw)
253 nose = cubic((cx - 0.02 * fw, cy + 0.14 * fh), (cx + 0.035 * fw, cy + 0.24 * fh), (cx + 0.03 * fw, cy + 0.31 * fh), (cx - 0.05 * fw, cy + 0.33 * fh), n=16)
254 d.line(nose, fill=rgba(mix(skin, "#000000", 0.32), 200), width=max(2, int(0.032 * fw)), joint="curve")
255 _smile(d, cx, cy + 0.52 * fh, fw, mix(skin, "#7a3b3b", 0.55), width=0.15)
256 base.alpha_composite(lay)
257
258
259 def anime_adult(base, cx, cy, s, skin, hair_c, cloth_c, iris_c, style="bob"):
260 """Original adult anime-style character, bright open eyes."""
261 fw, fh = s, s * 1.16
262 _figure_shadow(base, cx, cy, fw, fh, alpha=40)
263
264 # back hair
265 lay = new_layer()
266 d = ImageDraw.Draw(lay)
267 if style == "twin":
268 for sgn in (-1, 1):
269 tail = smooth_path([
270 ((cx + sgn * 0.85 * fw, cy - 0.7 * fh), (cx + sgn * 1.9 * fw, cy - 0.2 * fh), (cx + sgn * 2.1 * fw, cy + 1.2 * fh), (cx + sgn * 1.5 * fw, cy + 2.4 * fh)),
271 ((cx + sgn * 1.5 * fw, cy + 2.4 * fh), (cx + sgn * 1.2 * fw, cy + 1.4 * fh), (cx + sgn * 0.9 * fw, cy + 0.6 * fh), (cx + sgn * 0.72 * fw, cy - 0.2 * fh)),
272 ])
273 d.polygon(tail, fill=rgba(mix(hair_c, "#000000", 0.08), 255))
274 back = smooth_path([
275 ((cx - 1.08 * fw, cy - 0.3 * fh), (cx - 1.2 * fw, cy - 1.5 * fh), (cx + 0.9 * fw, cy - 1.6 * fh), (cx + 1.12 * fw, cy - 0.3 * fh)),
276 ((cx + 1.12 * fw, cy - 0.3 * fh), (cx + 1.3 * fw, cy + 0.9 * fh), (cx + 0.95 * fw, cy + 2.0 * fh), (cx + 0.55 * fw, cy + 2.5 * fh)),
277 ((cx + 0.55 * fw, cy + 2.5 * fh), (cx + 0.3 * fw, cy + 1.6 * fh), (cx - 0.1 * fw, cy + 2.6 * fh), (cx - 0.5 * fw, cy + 2.4 * fh)),
278 ((cx - 0.5 * fw, cy + 2.4 * fh), (cx - 1.15 * fw, cy + 1.2 * fh), (cx - 1.25 * fw, cy + 0.4 * fh), (cx - 1.08 * fw, cy - 0.3 * fh)),
279 ])
280 d.polygon(back, fill=rgba(hair_c, 255))
281 base.alpha_composite(lay)
282
283 lay = new_layer()
284 d = ImageDraw.Draw(lay)
285 d.polygon(_neck_pts(cx, cy, fw, fh), fill=rgba(mix(skin, "#000000", 0.06), 255))
286 sh = _shoulders_pts(cx, cy + 1.42 * fh, 3.1 * fw, 2.1 * fh)
287 d.polygon(sh, fill=rgba(cloth_c, 255))
288 # hoodie strings + collar
289 d.polygon([(cx - 0.44 * fw, cy + 1.5 * fh), (cx, cy + 2.05 * fh), (cx + 0.44 * fw, cy + 1.5 * fh), (cx + 0.30 * fw, cy + 1.38 * fh), (cx - 0.30 * fw, cy + 1.38 * fh)], fill=rgba(mix(cloth_c, "#000000", 0.12), 255))
290 d.line([(cx - 0.14 * fw, cy + 1.9 * fh), (cx - 0.16 * fw, cy + 2.3 * fh)], fill=rgba(mix(cloth_c, "#ffffff", 0.6), 255), width=max(2, int(0.045 * fw)))
291 d.line([(cx + 0.14 * fw, cy + 1.9 * fh), (cx + 0.16 * fw, cy + 2.3 * fh)], fill=rgba(mix(cloth_c, "#ffffff", 0.6), 255), width=max(2, int(0.045 * fw)))
292 base.alpha_composite(lay)
293
294 lay = new_layer()
295 d = ImageDraw.Draw(lay)
296 d.polygon(_face_outline(cx, cy, fw, fh), fill=rgba(skin, 255))
297 base.alpha_composite(lay)
298
299 # fringe with anime points
300 lay = new_layer()
301 d = ImageDraw.Draw(lay)
302 pts = []
303 x0, x1 = cx - 1.02 * fw, cx + 1.02 * fw
304 top_y = cy - 1.28 * fh
305 bot_y = cy - 0.42 * fh
306 n = 7
307 pts.append((x0, cy - 0.2 * fh))
308 pts.extend(cubic((x0, cy - 0.2 * fh), (x0 - 0.05 * fw, top_y - 0.2 * fh), (cx, top_y - 0.25 * fh), (x1, cy - 0.35 * fh), n=30)[1:])
309 pts.append((x1, cy - 0.1 * fh))
310 for i in range(n):
311 xm = x1 - (x1 - x0) * (i + 0.5) / n
312 xe = x1 - (x1 - x0) * (i + 1) / n
313 depth = bot_y + (0.10 * fh if i % 2 == 0 else -0.06 * fh)
314 pts.append((xm, depth))
315 pts.append((xe, bot_y - 0.08 * fh))
316 d.polygon(pts, fill=rgba(hair_c, 255))
317 base.alpha_composite(lay)
318
319 lay = new_layer()
320 d = ImageDraw.Draw(lay)
321 _open_eye(lay, cx - 0.42 * fw, cy + 0.05 * fh, fw, iris_c)
322 _open_eye(lay, cx + 0.42 * fw, cy + 0.05 * fh, fw, iris_c)
323 ink = mix(hair_c, "#000000", 0.45)
324 d.line(cubic((cx - 0.58 * fw, cy - 0.12 * fh), (cx - 0.45 * fw, cy - 0.22 * fh), (cx - 0.28 * fw, cy - 0.22 * fh), (cx - 0.24 * fw, cy - 0.14 * fh), n=16), fill=rgba(ink, 220), width=max(2, int(0.035 * fw)))
325 d.line(cubic((cx + 0.58 * fw, cy - 0.12 * fh), (cx + 0.45 * fw, cy - 0.22 * fh), (cx + 0.28 * fw, cy - 0.22 * fh), (cx + 0.24 * fw, cy - 0.14 * fh), n=16), fill=rgba(ink, 220), width=max(2, int(0.035 * fw)))
326 _smile(d, cx, cy + 0.5 * fh, fw, mix(skin, "#8a4040", 0.6), width=0.13)
327 _blush(lay, cx - 0.58 * fw, cy + 0.32 * fh, fw, (242, 153, 142), alpha=80)
328 _blush(lay, cx + 0.58 * fw, cy + 0.32 * fh, fw, (242, 153, 142), alpha=80)
329 base.alpha_composite(lay)
330
331
332 def silhouette_muse(base, cx, cy, s, body_c, rim_c=None):
333 """Elegant adult woman silhouette in a flowing gown (no facial features)."""
334 fw, fh = s, s * 1.2
335 lay = new_layer()
336 d = ImageDraw.Draw(lay)
337 # flowing gown
338 gown = smooth_path([
339 ((cx - 0.35 * fw, cy + 1.2 * fh), (cx - 1.6 * fw, cy + 2.2 * fh), (cx - 2.6 * fw, cy + 3.4 * fh), (cx - 2.9 * fw, cy + 4.6 * fh)),
340 ((cx - 2.9 * fw, cy + 4.6 * fh), (cx - 1.2 * fw, cy + 4.9 * fh), (cx + 1.4 * fw, cy + 4.9 * fh), (cx + 2.8 * fw, cy + 4.4 * fh)),
341 ((cx + 2.8 * fw, cy + 4.4 * fh), (cx + 2.2 * fw, cy + 3.0 * fh), (cx + 1.3 * fw, cy + 2.0 * fh), (cx + 0.38 * fw, cy + 1.2 * fh)),
342 ])
343 d.polygon(gown, fill=rgba(body_c, 255))
344 # torso + neck + head
345 d.polygon(_neck_pts(cx, cy, fw, fh), fill=rgba(body_c, 255))
346 torso = smooth_path([
347 ((cx - 0.9 * fw, cy + 2.2 * fh), (cx - 0.75 * fw, cy + 1.4 * fh), (cx - 0.4 * fw, cy + 1.25 * fh), (cx, cy + 1.2 * fh)),
348 ((cx, cy + 1.2 * fh), (cx + 0.4 * fw, cy + 1.25 * fh), (cx + 0.75 * fw, cy + 1.4 * fh), (cx + 0.9 * fw, cy + 2.2 * fh)),
349 ((cx + 0.9 * fw, cy + 2.2 * fh), (cx + 0.4 * fw, cy + 2.5 * fh), (cx - 0.4 * fw, cy + 2.5 * fh), (cx - 0.9 * fw, cy + 2.2 * fh)),
350 ])
351 d.polygon(torso, fill=rgba(body_c, 255))
352 d.polygon(_face_outline(cx, cy, fw * 0.92, fh * 0.92), fill=rgba(body_c, 255))
353 # flowing hair
354 hair = smooth_path([
355 ((cx - 0.95 * fw, cy - 0.35 * fh), (cx - 1.1 * fw, cy - 1.5 * fh), (cx + 0.9 * fw, cy - 1.6 * fh), (cx + 1.05 * fw, cy - 0.3 * fh)),
356 ((cx + 1.05 * fw, cy - 0.3 * fh), (cx + 1.9 * fw, cy + 0.6 * fh), (cx + 2.6 * fw, cy + 1.1 * fh), (cx + 3.2 * fw, cy + 0.9 * fh)),
357 ((cx + 3.2 * fw, cy + 0.9 * fh), (cx + 2.4 * fw, cy + 1.7 * fh), (cx + 1.6 * fw, cy + 1.7 * fh), (cx + 1.0 * fw, cy + 1.3 * fh)),
358 ((cx + 1.0 * fw, cy + 1.3 * fh), (cx + 0.5 * fw, cy + 0.9 * fh), (cx - 0.2 * fw, cy + 1.1 * fh), (cx - 0.6 * fw, cy + 0.7 * fh)),
359 ((cx - 0.6 * fw, cy + 0.7 * fh), (cx - 1.05 * fw, cy + 0.3 * fh), (cx - 1.0 * fw, cy - 0.05 * fh), (cx - 0.95 * fw, cy - 0.35 * fh)),
360 ])
361 d.polygon(hair, fill=rgba(mix(body_c, "#000000", 0.12), 255))
362 base.alpha_composite(lay)
363 if rim_c:
364 rim = new_layer()
365 rd = ImageDraw.Draw(rim)
366 rd.line(cubic((cx + 0.9 * fw, cy - 0.6 * fh), (cx + 1.2 * fw, cy + 0.4 * fh), (cx + 1.1 * fw, cy + 1.6 * fh), (cx + 0.8 * fw, cy + 2.2 * fh), n=40), fill=rgba(rim_c, 150), width=max(3, int(0.07 * fw)))
367 rd.line(cubic((cx + 1.05 * fw, cy - 0.3 * fh), (cx + 2.0 * fw, cy + 0.7 * fh), (cx + 2.8 * fw, cy + 1.2 * fh), (cx + 3.1 * fw, cy + 1.0 * fh), n=40), fill=rgba(rim_c, 120), width=max(3, int(0.06 * fw)))
368 comp(base, rim, 6)
369
370
371 def mascot_programmer(base, cx, cy, s, skin, hair_c, cloth_c, accent_c, glasses_c=None):
372 """Original cheerful 'lucky programmer' mascot — fictional adult coder."""
373 fw, fh = s, s * 1.14
374 glasses_c = glasses_c or mix(cloth_c, "#000000", 0.5)
375 _figure_shadow(base, cx, cy, fw, fh, alpha=42)
376
377 lay = new_layer()
378 d = ImageDraw.Draw(lay)
379 d.polygon(_neck_pts(cx, cy, fw, fh), fill=rgba(mix(skin, "#000000", 0.07), 255))
380 sh = _shoulders_pts(cx, cy + 1.42 * fh, 3.5 * fw, 2.3 * fh)
381 d.polygon(sh, fill=rgba(cloth_c, 255))
382 # hoodie collar + pocket + strings
383 d.polygon([(cx - 0.5 * fw, cy + 1.5 * fh), (cx, cy + 2.0 * fh), (cx + 0.5 * fw, cy + 1.5 * fh), (cx + 0.36 * fw, cy + 1.34 * fh), (cx - 0.36 * fw, cy + 1.34 * fh)], fill=rgba(mix(cloth_c, "#000000", 0.14), 255))
384 d.line([(cx - 0.16 * fw, cy + 1.85 * fh), (cx - 0.20 * fw, cy + 2.35 * fh)], fill=rgba(accent_c, 255), width=max(3, int(0.05 * fw)))
385 d.line([(cx + 0.16 * fw, cy + 1.85 * fh), (cx + 0.20 * fw, cy + 2.35 * fh)], fill=rgba(accent_c, 255), width=max(3, int(0.05 * fw)))
386 pocket = superellipse_poly(cx, cy + 2.9 * fh, 0.7 * fw, 0.5 * fh, power=5)
387 d.polygon(pocket, fill=rgba(mix(cloth_c, "#ffffff", 0.10), 255))
388 base.alpha_composite(lay)
389
390 # headphones around neck
391 lay = new_layer()
392 d = ImageDraw.Draw(lay)
393 band = cubic((cx - 0.75 * fw, cy + 1.35 * fh), (cx - 0.5 * fw, cy + 1.9 * fh), (cx + 0.5 * fw, cy + 1.9 * fh), (cx + 0.75 * fw, cy + 1.35 * fh), n=40)
394 d.line(band, fill=rgba(glasses_c, 255), width=max(4, int(0.14 * fw)))
395 for sgn in (-1, 1):
396 d.ellipse([cx + sgn * 0.82 * fw - 0.17 * fw, cy + 1.18 * fh, cx + sgn * 0.82 * fw + 0.17 * fw, cy + 1.62 * fh], fill=rgba(glasses_c, 255))
397 d.ellipse([cx + sgn * 0.82 * fw - 0.09 * fw, cy + 1.28 * fh, cx + sgn * 0.82 * fw + 0.09 * fw, cy + 1.52 * fh], fill=rgba(accent_c, 255))
398 base.alpha_composite(lay)
399
400 lay = new_layer()
401 d = ImageDraw.Draw(lay)
402 d.polygon(_face_outline(cx, cy, fw, fh), fill=rgba(skin, 255))
403 base.alpha_composite(lay)
404
405 # short tidy hair
406 lay = new_layer()
407 d = ImageDraw.Draw(lay)
408 hair = smooth_path([
409 ((cx - 0.98 * fw, cy - 0.28 * fh), (cx - 1.07 * fw, cy - 1.22 * fh), (cx + 0.62 * fw, cy - 1.40 * fh), (cx + 0.99 * fw, cy - 0.38 * fh)),
410 ((cx + 0.99 * fw, cy - 0.38 * fh), (cx + 0.88 * fw, cy - 0.62 * fh), (cx + 0.58 * fw, cy - 0.80 * fh), (cx + 0.28 * fw, cy - 0.72 * fh)),
411 ((cx + 0.28 * fw, cy - 0.72 * fh), (cx - 0.06 * fw, cy - 0.88 * fh), (cx - 0.5 * fw, cy - 0.82 * fh), (cx - 0.70 * fw, cy - 0.54 * fh)),
412 ((cx - 0.70 * fw, cy - 0.54 * fh), (cx - 0.88 * fw, cy - 0.64 * fh), (cx - 0.97 * fw, cy - 0.48 * fh), (cx - 0.98 * fw, cy - 0.28 * fh)),
413 ])
414 d.polygon(hair, fill=rgba(hair_c, 255))
415 base.alpha_composite(lay)
416
417 # face: happy closed eyes (^^), round glasses
418 lay = new_layer()
419 d = ImageDraw.Draw(lay)
420 ink = mix(skin, "#000000", 0.78)
421 for sgn in (-1, 1):
422 ex = cx + sgn * 0.42 * fw
423 d.ellipse([ex - 0.26 * fw, cy - 0.20 * fh, ex + 0.26 * fw, cy + 0.30 * fh], outline=rgba(glasses_c, 235), width=max(3, int(0.045 * fw)))
424 pts = cubic((ex - 0.14 * fw, cy + 0.08 * fh), (ex - 0.05 * fw, cy - 0.03 * fh), (ex + 0.05 * fw, cy - 0.03 * fh), (ex + 0.14 * fw, cy + 0.08 * fh), n=20)
425 d.line(pts, fill=rgba(ink, 240), width=max(3, int(0.045 * fw)), joint="curve")
426 d.line([(cx - 0.16 * fw, cy + 0.05 * fh), (cx + 0.16 * fw, cy + 0.05 * fh)], fill=rgba(glasses_c, 235), width=max(3, int(0.04 * fw)))
427 d.line([(cx - 0.68 * fw, cy + 0.02 * fh), (cx - 0.88 * fw, cy - 0.05 * fh)], fill=rgba(glasses_c, 235), width=max(3, int(0.04 * fw)))
428 d.line([(cx + 0.68 * fw, cy + 0.02 * fh), (cx + 0.88 * fw, cy - 0.05 * fh)], fill=rgba(glasses_c, 235), width=max(3, int(0.04 * fw)))
429 # open happy mouth
430 mouth = smooth_path([
431 ((cx - 0.16 * fw, cy + 0.5 * fh), (cx - 0.05 * fw, cy + 0.52 * fh), (cx + 0.05 * fw, cy + 0.52 * fh), (cx + 0.16 * fw, cy + 0.5 * fh)),
432 ((cx + 0.16 * fw, cy + 0.5 * fh), (cx + 0.08 * fw, cy + 0.68 * fh), (cx - 0.08 * fw, cy + 0.68 * fh), (cx - 0.16 * fw, cy + 0.5 * fh)),
433 ])
434 d.polygon(mouth, fill=rgba(mix(skin, "#7a2e2e", 0.6), 235))
435 _blush(lay, cx - 0.62 * fw, cy + 0.34 * fh, fw, (238, 140, 110), alpha=85)
436 _blush(lay, cx + 0.62 * fw, cy + 0.34 * fh, fw, (238, 140, 110), alpha=85)
437 base.alpha_composite(lay)
438
439
440 def performer(base, cx, cy, s, skin, hair_c, cloth_c, glow_c):
441 """Original adult digital stage performer with headset mic, high ponytail."""
442 fw, fh = s, s * 1.18
443 _figure_shadow(base, cx, cy, fw, fh, alpha=44)
444
445 # ponytail
446 lay = new_layer()
447 d = ImageDraw.Draw(lay)
448 tail = smooth_path([
449 ((cx + 0.55 * fw, cy - 1.35 * fh), (cx + 1.6 * fw, cy - 1.2 * fh), (cx + 2.1 * fw, cy + 0.2 * fh), (cx + 1.85 * fw, cy + 1.6 * fh)),
450 ((cx + 1.85 * fw, cy + 1.6 * fh), (cx + 1.25 * fw, cy + 0.9 * fh), (cx + 0.85 * fw, cy + 0.2 * fh), (cx + 0.7 * fw, cy - 0.6 * fh)),
451 ])
452 d.polygon(tail, fill=rgba(mix(hair_c, "#000000", 0.10), 255))
453 base.alpha_composite(lay)
454
455 lay = new_layer()
456 d = ImageDraw.Draw(lay)
457 d.polygon(_neck_pts(cx, cy, fw, fh), fill=rgba(mix(skin, "#000000", 0.07), 255))
458 sh = _shoulders_pts(cx, cy + 1.45 * fh, 3.3 * fw, 2.2 * fh)
459 d.polygon(sh, fill=rgba(cloth_c, 255))
460 # jacket collar wings
461 d.polygon([(cx - 0.46 * fw, cy + 1.5 * fh), (cx - 0.12 * fw, cy + 2.1 * fh), (cx - 0.55 * fw, cy + 2.0 * fh), (cx - 0.75 * fw, cy + 1.65 * fh)], fill=rgba(mix(cloth_c, glow_c, 0.25), 255))
462 d.polygon([(cx + 0.46 * fw, cy + 1.5 * fh), (cx + 0.12 * fw, cy + 2.1 * fh), (cx + 0.55 * fw, cy + 2.0 * fh), (cx + 0.75 * fw, cy + 1.65 * fh)], fill=rgba(mix(cloth_c, glow_c, 0.25), 255))
463 base.alpha_composite(lay)
464
465 lay = new_layer()
466 d = ImageDraw.Draw(lay)
467 d.polygon(_face_outline(cx, cy, fw, fh), fill=rgba(skin, 255))
468 base.alpha_composite(lay)
469
470 # swept hair + fringe
471 lay = new_layer()
472 d = ImageDraw.Draw(lay)
473 hair = smooth_path([
474 ((cx - 1.0 * fw, cy - 0.3 * fh), (cx - 1.1 * fw, cy - 1.3 * fh), (cx + 0.7 * fw, cy - 1.5 * fh), (cx + 1.05 * fw, cy - 0.4 * fh)),
475 ((cx + 1.05 * fw, cy - 0.4 * fh), (cx + 0.8 * fw, cy - 0.7 * fh), (cx + 0.5 * fw, cy - 0.9 * fh), (cx + 0.2 * fw, cy - 0.82 * fh)),
476 ((cx + 0.2 * fw, cy - 0.82 * fh), (cx - 0.2 * fw, cy - 1.0 * fh), (cx - 0.6 * fw, cy - 0.9 * fh), (cx - 0.8 * fw, cy - 0.6 * fh)),
477 ((cx - 0.8 * fw, cy - 0.6 * fh), (cx - 0.95 * fw, cy - 0.7 * fh), (cx - 1.0 * fw, cy - 0.5 * fh), (cx - 1.0 * fw, cy - 0.3 * fh)),
478 ])
479 d.polygon(hair, fill=rgba(hair_c, 255))
480 base.alpha_composite(lay)
481
482 # headset: band over crown + mic boom to mouth
483 lay = new_layer()
484 d = ImageDraw.Draw(lay)
485 band = cubic((cx - 0.85 * fw, cy - 0.45 * fh), (cx - 0.6 * fw, cy - 1.45 * fh), (cx + 0.6 * fw, cy - 1.45 * fh), (cx + 0.85 * fw, cy - 0.45 * fh), n=40)
486 d.line(band, fill=rgba(mix(cloth_c, "#000000", 0.3), 255), width=max(3, int(0.09 * fw)))
487 d.ellipse([cx + 0.72 * fw, cy - 0.28 * fh, cx + 1.02 * fw, cy + 0.12 * fh], fill=rgba(mix(cloth_c, "#000000", 0.3), 255))
488 boom = cubic((cx + 0.88 * fw, cy + 0.05 * fh), (cx + 0.95 * fw, cy + 0.45 * fh), (cx + 0.5 * fw, cy + 0.62 * fh), (cx + 0.28 * fw, cy + 0.58 * fh), n=30)
489 d.line(boom, fill=rgba(mix(cloth_c, "#000000", 0.3), 255), width=max(2, int(0.05 * fw)))
490 d.ellipse([cx + 0.18 * fw, cy + 0.5 * fh, cx + 0.34 * fw, cy + 0.66 * fh], fill=rgba(glow_c, 255))
491
492 ink = mix(skin, "#000000", 0.72)
493 _closed_eye(d, cx - 0.42 * fw, cy + 0.0 * fh, fw, ink, lashes=2)
494 _open_eye(lay, cx + 0.42 * fw, cy + 0.02 * fh, fw, mix(glow_c, "#000000", 0.25))
495 _brow(d, cx - 0.42 * fw, cy - 0.28 * fh, fw, mix(hair_c, "#000000", 0.25))
496 _brow(d, cx + 0.42 * fw, cy - 0.28 * fh, fw, mix(hair_c, "#000000", 0.25))
497 _smile(d, cx, cy + 0.52 * fh, fw, mix(skin, "#8a4040", 0.6), width=0.15)
498 _blush(lay, cx - 0.56 * fw, cy + 0.3 * fh, fw, glow_c, alpha=55)
499 _blush(lay, cx + 0.56 * fw, cy + 0.3 * fh, fw, glow_c, alpha=55)
500 base.alpha_composite(lay)
501
501 lines PYTHON