先试试看
打开鹈鹕骑单车 Demo,鹈鹕已经在路上了。拖动「车速」滑块能让它从散步骑到冲刺,点「按铃」它会叮铃一声,点「显示骨架」可以看到双腿是怎么被踏板带着走的。右上角切换到深色模式,白天的路就变成了夜骑。整个画面是一张内联 SVG,没有任何图片文件。
为什么是鹈鹕
「Generate an SVG of a pelican riding a bicycle」是开发者 Simon Willison 测试大模型用的一句提示词,这些年几乎每个新模型发布,都会有人拿它画一只鹈鹕出来比一比。它好用,是因为这个题目刁钻:鹈鹕的身材根本不适合骑车,模型没法照搬现成的图,只能自己想清楚车架长什么样、鸟坐在哪儿、脚怎么够到踏板。画出来的结果常常是车架散架、鸟浮在半空,或者腿压根没碰到踏板。
静态图已经被画过很多遍了,所以这次我们把难度往上加了一格:让它动起来,而且要动得对。
我是怎样描述需求的
动画比静态图多出来的难点,全在「各部分之间的约束」上,所以我把这些约束一条条写进了提示词:
请创建 pelican-bicycle 静态 Web Demo:一只鹈鹕骑自行车的 2D SVG 动画,纯内联 SVG,不使用任何图片。要求:车架结构合理(后轮、中轴、车座、车头、前叉连成真实的菱形车架);鹈鹕要有明显特征——长喙和下方的大喉囊、白色身体、翅膀搭在车把上;双脚必须始终踩在踏板上,腿用大腿和小腿两段,随曲柄转动实时解算膝盖位置,膝盖朝车头方向弯;车轮转速、链条移动和踏频按齿比同步;身体随踩踏轻微上下起伏,喉囊比身体慢半拍晃动;背景的云、远山、近山、路面分层视差滚动。提供暂停、车速滑块、按铃和显示骨架开关,显示踏频、车速、骑行距离。用原生 HTML、CSS、JavaScript,沿用米白纸张与深墨色的视觉风格,支持深色模式、手机适配,并在系统开启减少动态效果时默认暂停。完成后逐角度检查一整圈曲柄旋转:腿长够不够、膝盖有没有反折。
最后一句是重点。腿是这类动画最容易穿帮的地方,我不想靠肉眼盯着看它有没有哪一帧抽筋。
使用时你会看到什么
页面中间是一张 640×400 的画布:红色公路车、白色鹈鹕、橙色长喙和一只大得离谱的喉囊,翅膀尖正好搭在车把上。脚踩着踏板一圈圈转,远侧那条腿颜色稍淡,藏在车架后面。每蹬一下身体微微一沉,喉囊跟着晚半拍晃一下,像兜着一口水。车轮辐条、链条的虚线和路面标线一起往后走,远山比近山移得慢,云最慢。
下方三个数字分别是踏频、车速和累计里程。默认 66 rpm,按画面里牙盘与飞轮 2:1 的齿比、700c 车轮换算,大约是 16.6 km/h;滑块拉到 3 倍就接近 50 km/h,这只鹈鹕体力不错。
先定体验,再选技术
我先定了一条原则:画面上每个会动的东西,都从同一个量推出来。这个量就是曲柄角度 theta。每一帧只让 theta 按踏频往前走一点,其余全部由它算出来:
- 踏板位置 = 中轴 + 曲柄长 × (cos θ, sin θ),远侧踏板再加 π;
- 车轮转角 = θ × 齿比 2;
- 链条虚线偏移 = θ × 牙盘半径;
- 路面位移 = 车轮转角 × 车轮半径,远山、近山、云按 0.2、0.45、0.08 的系数跟着走;
- 身体起伏 = −1.8 × cos 2θ,每蹬一脚沉一次。
这样暂停、变速、切后台再回来都不会出现「脚在转车轮没转」这种不同步,因为它们本来就没有各自的时钟。技术上是原生 HTML、CSS、JavaScript,一个 requestAnimationFrame 循环直接改 SVG 属性,没有动画库。颜色全部写成 CSS 变量,深色模式只是换一组变量值,太阳换成月亮用的也是同一个圆。
腿怎么跟着踏板走
让腿「贴着」踏板有个偷懒办法:画一条从髋到踏板的直线。但那样腿会像弹簧一样伸缩,一眼就假。真实的腿长度不变,靠膝盖弯曲来适应距离变化,这是一个经典的两段式反向运动学(IK)问题:已知髋和脚的位置、大腿和小腿的长度,求膝盖在哪。用余弦定理就能算:
function solveKnee(hip, foot) {
const dx = foot.x - hip.x, dy = foot.y - hip.y;
const d = Math.min(Math.hypot(dx, dy), THIGH + SHIN - 0.001);
const cosA = (THIGH * THIGH + d * d - SHIN * SHIN) / (2 * THIGH * d);
const a = Math.acos(Math.max(-1, Math.min(1, cosA)));
const base = Math.atan2(dy, dx);
return { x: hip.x + THIGH * Math.cos(base - a), y: hip.y + THIGH * Math.sin(base - a) };
}
两个细节决定了它看起来像不像:
- 膝盖朝哪边弯。 三角形有两个解,膝盖可以朝前也可以朝后。这里固定用
base - a那一支,让膝盖永远朝车头方向,鸟腿才不会某一帧突然反折成火烈鸟。 - 腿够不够长。
Math.min那一行是保险,真正的保证来自几何参数本身:车座高度、中轴位置、曲柄长度和腿长要配得上。如果踏板转到最低点时髋到脚的距离超过大腿加小腿,腿会被拉直锁死,看起来像踮脚踩空。
打开「显示骨架」就能看到蓝色的髋、膝、踝三点和曲柄轨迹圆,它们每一帧都在重算。
验证与边界
那句「逐角度检查一整圈」对应一段 Node 脚本:直接加载 app.js 里的几何函数,把曲柄从 0° 转到 359°,两条腿各算一遍,同时叠加身体起伏。结果是:髋到脚的距离在 78.8 到 123.0 之间,腿长上限是 136,一次都没有触发拉直保护;720 个姿态里膝盖朝后弯的次数是 0;小腿长度误差在 10⁻¹³ 量级,也就是说腿在整圈里确实没有伸缩。
浏览器里我在桌面宽度、375px 手机宽度、浅色和深色模式下都截了图。第一版确实有一处问题:脖子只画了一条白色粗线,没有描边,和身体接在一起的地方看不出轮廓,像是另外贴上去的一截。修法是先画一条更粗的深色线当描边,再叠白色,并把脖子挪到身体下面一层,让身体盖住接缝。改完再截一次图,脖子和身体才像同一只鸟。
目前有几条明确的边界:
- 视角固定在正侧面,鹈鹕不会转弯、上坡或者下车;
- 路是无限平路,没有坡度,也就没有「用力蹬」和「滑行」的区别;
- 翅膀是静态的,只随身体起伏平移,不会扶着车把用力;
- 开启系统「减少动态效果」时默认暂停,需要手动点「出发」。
可复用经验
这次最值得带走的不是鹈鹕,是写动画需求的方法:
- 找出唯一的驱动量。 把所有会动的部分都写成它的函数,而不是给每个部分各配一个计时器。同步问题会在设计阶段就消失。
- 在提示词里写约束,而不只写外观。 「脚始终踩在踏板上」「膝盖朝前弯」「按齿比同步」这种句子,比「画得可爱一点」更能决定成品质量。
- 让验证可以量化。 「看起来没问题」不够,「一整圈 720 个姿态里反折 0 次」才算确认。把检查方式写进需求,Clidex 就会把它当成交付的一部分。
想从零开始规划一个这样的小 Demo,可以先读做第一个 Demo 前,先把需求写清楚。