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