lerp666/cricket-commons
0
1"""参数化 SVG 渲染: traits + age + stage + sick + mood → 蛐蛐形象.2 3设计原则: 零依赖、零成本、演化连续可见。4映射(可随时调,但保持单调直觉):5 勇 brave → 触须张角/前肢姿态(越勇越张扬)6 萌 cute → 眼睛大小/身体圆润度7 怨 grudge → 体色向紫黑偏移/眉角下压8 智 wit → 额头花纹复杂度(条纹数)9 馋 glutton → 肚子大小10 stage → 整体尺寸 + 翅膀层数(蜕皮=大变样)11 sick → 整体降饱和 + 绿色病气 + 眼睛变 X12"""13from __future__ import annotations14 15from traits import TRAIT_KEYS, molt_stage16 17 18def _lerp(a: float, b: float, t: float) -> float:19 return a + (b - a) * t20 21 22def _color(traits: dict, sick: bool) -> tuple[str, str]:23 """主体色: 健康=暖棕→怨气高偏紫黑; 生病=灰绿."""24 g = traits.get("grudge", 0.5)25 if sick:26 return "#7a8a6a", "#5a6a4a"27 r = int(_lerp(150, 90, g))28 gr = int(_lerp(95, 60, g))29 b = int(_lerp(60, 110, g))30 return f"rgb({r},{gr},{b})", f"rgb({max(r-40,0)},{max(gr-30,0)},{max(b-20,0)})"31 32 33def render_svg(traits: dict, feed_count: int = 0, sick: bool = False,34 mood: str = "calm", width: int = 360, height: int = 280) -> str:35 """返回完整 <svg> 字符串."""36 t = {k: float(traits.get(k, 0.5)) for k in TRAIT_KEYS}37 stage = molt_stage(feed_count)38 scale = _lerp(0.7, 1.15, min(stage, 3) / 3)39 body_fill, body_dark = _color(t, sick)40 41 cx, cy = width / 2, height / 2 + 2042 belly = _lerp(34, 52, t["glutton"]) * scale # 馋→肚子43 body_ry = _lerp(belly * 0.78, belly * 0.95, t["cute"]) # 萌→圆润44 eye_r = _lerp(4.5, 9.5, t["cute"]) * scale # 萌→大眼45 ant_spread = _lerp(8, 42, t["brave"]) # 勇→触须张角(度)46 ant_len = _lerp(55, 85, t["brave"]) * scale47 stripes = 1 + int(round(t["wit"] * 4)) # 智→花纹条数48 brow_drop = _lerp(-3, 5, t["grudge"]) # 怨→眉角下压49 50 head_cx = cx - belly * 0.9551 head_cy = cy - body_ry * 0.4552 head_r = _lerp(16, 22, t["cute"]) * scale53 54 parts: list[str] = []55 parts.append(56 f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '57 f'viewBox="0 0 {width} {height}">'58 )59 # 地面60 parts.append(f'<ellipse cx="{cx}" cy="{cy + body_ry + 14}" rx="{belly * 1.6}" ry="10" fill="#00000014"/>')61 # 后腿(蛐蛐标志性大跳腿)62 leg = belly * 1.2563 parts.append(64 f'<path d="M {cx + belly * 0.5} {cy} q {leg * 0.55} {-leg * 0.7} {leg * 0.32} {leg * 0.05} '65 f'l {-leg * 0.12} {leg * 0.62}" stroke="{body_dark}" stroke-width="{5 * scale}" fill="none" stroke-linecap="round"/>'66 )67 # 身体68 parts.append(f'<ellipse cx="{cx}" cy="{cy}" rx="{belly}" ry="{body_ry}" fill="{body_fill}"/>')69 # 翅膀层数 = stage+1(蜕皮可见)70 for i in range(stage + 1):71 wy = cy - body_ry * 0.55 - i * 4 * scale72 parts.append(73 f'<ellipse cx="{cx + 6}" cy="{wy}" rx="{belly * 0.85}" ry="{body_ry * 0.45}" '74 f'fill="{body_dark}" opacity="{0.55 - i * 0.12}"/>'75 )76 # 智力花纹77 for i in range(stripes):78 sx = cx - belly * 0.4 + i * (belly * 0.8 / max(stripes - 1, 1))79 parts.append(80 f'<line x1="{sx}" y1="{cy - body_ry * 0.25}" x2="{sx}" y2="{cy + body_ry * 0.35}" '81 f'stroke="{body_dark}" stroke-width="{2 * scale}" opacity="0.6"/>'82 )83 # 头84 parts.append(f'<circle cx="{head_cx}" cy="{head_cy}" r="{head_r}" fill="{body_fill}"/>')85 # 触须(勇→张角)86 for sign in (-1, 1):87 ang = (-90 + sign * ant_spread) * 3.14159 / 18088 ex = head_cx + ant_len * 1.0 * __import__("math").cos(ang) * (0.6 if sign < 0 else 0.8)89 ey = head_cy + ant_len * __import__("math").sin(ang)90 mx = head_cx + sign * 1091 my = head_cy - ant_len * 0.692 parts.append(93 f'<path d="M {head_cx} {head_cy - head_r * 0.7} Q {mx} {my} {ex} {ey}" '94 f'stroke="{body_dark}" stroke-width="{2.2 * scale}" fill="none" stroke-linecap="round"/>'95 )96 # 眼睛 / 病眼97 eye_x, eye_y = head_cx - head_r * 0.25, head_cy - head_r * 0.1598 if sick:99 s = eye_r * 0.8100 parts.append(101 f'<g stroke="#3a3a3a" stroke-width="2.4" stroke-linecap="round">'102 f'<line x1="{eye_x - s}" y1="{eye_y - s}" x2="{eye_x + s}" y2="{eye_y + s}"/>'103 f'<line x1="{eye_x - s}" y1="{eye_y + s}" x2="{eye_x + s}" y2="{eye_y - s}"/></g>'104 )105 # 病气泡106 parts.append(f'<circle cx="{head_cx - head_r * 1.4}" cy="{head_cy - head_r}" r="5" fill="#9ab87a" opacity="0.8"/>')107 else:108 parts.append(f'<circle cx="{eye_x}" cy="{eye_y}" r="{eye_r}" fill="#241a12"/>')109 parts.append(f'<circle cx="{eye_x + eye_r * 0.3}" cy="{eye_y - eye_r * 0.3}" r="{eye_r * 0.3}" fill="#fff"/>')110 # 怨气眉111 parts.append(112 f'<line x1="{eye_x - eye_r * 1.4}" y1="{eye_y - eye_r * 1.6 + brow_drop}" '113 f'x2="{eye_x + eye_r * 1.2}" y2="{eye_y - eye_r * 1.6 - brow_drop}" '114 f'stroke="#241a12" stroke-width="2.6" stroke-linecap="round"/>'115 )116 # 嘴(mood 简单弯曲)117 smile = {"happy": 6, "excited": 7, "loved": 6, "content": 4, "calm": 1,118 "sad": -5, "angry": -6, "hurt": -6, "disgusted": -4}.get(mood, 1)119 parts.append(120 f'<path d="M {head_cx - 7} {head_cy + head_r * 0.45} q 7 {smile} 14 0" '121 f'stroke="#241a12" stroke-width="2.2" fill="none" stroke-linecap="round"/>'122 )123 parts.append("</svg>")124 return "".join(parts)125 126 127def svg_params(traits: dict, feed_count: int, sick: bool, mood: str) -> dict:128 """快照里存的渲染参数(回放史料)."""129 return {130 "traits": {k: round(float(traits.get(k, 0.5)), 4) for k in TRAIT_KEYS},131 "stage": molt_stage(feed_count),132 "sick": bool(sick),133 "mood": mood,134 "feed_count": feed_count,135 }136 