昨天有幸尝了一次, 爽用了2个多小时… 还做了鹈鹕骑车测试
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>鹈鹕骑车</title>
<style>
body {
margin: 0; min-height: 100vh;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: linear-gradient(180deg, #1d2b53, #3a4a7a);
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
}
h1 {
color: #fff; font-weight: 600; letter-spacing: .12em;
margin: 18px 0 12px; font-size: 22px;
text-shadow: 0 2px 8px rgba(0,0,0,.4);
}
.stage {
width: min(94vw, 960px);
border-radius: 18px; overflow: hidden;
box-shadow: 0 18px 50px rgba(0,0,0,.45);
}
svg { display: block; width: 100%; height: auto; }
.hint { color: #9fb0d8; font-size: 13px; margin: 12px 0 22px; }
/* 太阳光芒缓慢自转 */
#rays {
animation: sunspin 26s linear infinite;
transform-box: fill-box; transform-origin: center;
}
@keyframes sunspin { to { transform: rotate(360deg); } }
/* 云朵飘动 */
.cloud { animation: drift linear infinite; }
#cloud1 { --y: 78px; --s: 1; animation-duration: 36s; animation-delay: -6s; }
#cloud2 { --y: 138px; --s: .68; animation-duration: 50s; animation-delay: -26s; }
#cloud3 { --y: 48px; --s: .5; animation-duration: 62s; animation-delay: -44s; }
@keyframes drift {
from { transform: translate(980px, var(--y)) scale(var(--s)); }
to { transform: translate(-320px, var(--y)) scale(var(--s)); }
}
/* 远山两层视差 */
#hillsFar { animation: scroll900 30s linear infinite; }
#hillsNear { animation: scroll900 15s linear infinite; }
/* 路边灌木贴地速度 */
#bushLoop { animation: scroll900 1.65s linear infinite; }
@keyframes scroll900 { from { transform: translateX(0); } to { transform: translateX(-900px); } }
/* 车后速度线 */
.whoosh { stroke: #fff; stroke-width: 4; stroke-linecap: round; opacity: 0; animation: whoosh .85s linear infinite; }
.whoosh.w2 { animation-delay: .28s; }
.whoosh.w3 { animation-delay: .55s; }
@keyframes whoosh {
0% { transform: translateX(50px); opacity: 0; }
25% { opacity: .55; }
100% { transform: translateX(-170px); opacity: 0; }
}
</style>
</head>
<body>
<h1>🚴 鹈鹕骑车</h1>
<div class="stage">
<svg viewBox="0 0 900 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="鹈鹕骑自行车动画">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#8fd3ff"/>
<stop offset="1" stop-color="#eaf9ff"/>
</linearGradient>
<g id="cloudShape" fill="#ffffff" opacity=".92">
<ellipse cx="0" cy="0" rx="46" ry="18"/>
<ellipse cx="-32" cy="6" rx="26" ry="13"/>
<ellipse cx="32" cy="7" rx="28" ry="14"/>
<ellipse cx="2" cy="-12" rx="30" ry="15"/>
</g>
<path id="hillShapeF" d="M0 530 L0 472 C 50 472, 80 424, 150 424 C 220 424, 250 470, 320 470 C 390 470, 410 430, 480 430 C 550 430, 570 468, 640 468 C 710 468, 730 434, 800 434 C 855 434, 880 472, 900 472 L900 530 Z"/>
<path id="hillShapeN" d="M0 530 L0 492 C 60 492, 90 452, 170 452 C 250 452, 280 494, 360 494 C 440 494, 460 456, 540 456 C 620 456, 650 496, 730 496 C 790 496, 860 492, 900 492 L900 530 Z"/>
<g id="bushShape">
<ellipse cx="120" cy="516" rx="22" ry="11" fill="#58a862"/>
<ellipse cx="104" cy="519" rx="14" ry="8" fill="#4c9a57"/>
<ellipse cx="137" cy="519" rx="13" ry="8" fill="#62b26c"/>
<rect x="416" y="490" width="8" height="26" rx="2" fill="#8a6239"/>
<circle cx="420" cy="484" r="19" fill="#58a862"/>
<circle cx="407" cy="492" r="12" fill="#4c9a57"/>
<circle cx="433" cy="492" r="12" fill="#62b26c"/>
<ellipse cx="700" cy="518" rx="18" ry="9" fill="#4c9a57"/>
<ellipse cx="716" cy="520" rx="11" ry="7" fill="#58a862"/>
</g>
</defs>
<!-- 天空与太阳 -->
<rect width="900" height="600" fill="url(#sky)"/>
<g transform="translate(790 95)">
<circle r="58" fill="#ffd54d" opacity=".18"/>
<g id="rays" stroke="#f7c548" stroke-width="6" stroke-linecap="round">
<line y1="-50" y2="-64"/>
<line y1="50" y2="64"/>
<line x1="-50" x2="-64"/>
<line x1="50" x2="64"/>
<line x1="-35" y1="-35" x2="-45" y2="-45"/>
<line x1="35" y1="35" x2="45" y2="45"/>
<line x1="-35" y1="35" x2="-45" y2="45"/>
<line x1="35" y1="-35" x2="45" y2="-45"/>
</g>
<circle r="40" fill="#ffd75e" stroke="#f5b62e" stroke-width="3"/>
</g>
<!-- 云 -->
<g class="cloud" id="cloud1"><use href="#cloudShape"/></g>
<g class="cloud" id="cloud2"><use href="#cloudShape"/></g>
<g class="cloud" id="cloud3"><use href="#cloudShape"/></g>
<!-- 远山(两层视差,各自首尾相接循环) -->
<g id="hillsFar" fill="#bfe0b0">
<use href="#hillShapeF"/><use href="#hillShapeF" x="900"/>
</g>
<g id="hillsNear" fill="#a2d38e">
<use href="#hillShapeN"/><use href="#hillShapeN" x="900"/>
</g>
<!-- 草地与灌木 -->
<rect x="0" y="506" width="900" height="24" fill="#7ec96f"/>
<g id="bushLoop">
<use href="#bushShape"/><use href="#bushShape" x="900"/>
</g>
<!-- 马路 -->
<rect x="0" y="530" width="900" height="70" fill="#4b4e57"/>
<rect x="0" y="533" width="900" height="3" fill="#7d828e"/>
<path id="roadDash" d="M0 567 H900" stroke="#eef0f4" stroke-width="5" stroke-dasharray="46 44"/>
<!-- 车轮阴影 -->
<ellipse cx="295" cy="532" rx="74" ry="6" fill="#000" opacity=".15"/>
<ellipse cx="565" cy="532" rx="74" ry="6" fill="#000" opacity=".15"/>
<ellipse cx="430" cy="532" rx="56" ry="5" fill="#000" opacity=".1"/>
<!-- 速度线 -->
<g>
<line class="whoosh" x1="215" y1="262" x2="258" y2="262"/>
<line class="whoosh w2" x1="195" y1="316" x2="245" y2="316"/>
<line class="whoosh w3" x1="225" y1="372" x2="262" y2="372"/>
</g>
<!-- 远侧腿 + 远侧曲柄脚踏(画在车架后面) -->
<path id="legF" d="" fill="none" stroke="#cd8028" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
<line id="crankF" x1="425" y1="462" x2="425" y2="422" stroke="#7c828d" stroke-width="7" stroke-linecap="round"/>
<rect id="pedF" x="-14" y="-5" width="28" height="10" rx="3" fill="#565b64"/>
<path id="footF" d="M-4 -2 C 2 -8, 14 -8, 22 -5 L 30 -1 L 22 1 L 28 4 L 19 5 L 22 10 L 8 8 C 0 7, -6 4, -4 -2 Z" fill="#d18f2a" stroke="#b3761c" stroke-width="1.5"/>
<!-- 后轮 -->
<g transform="translate(295 460)">
<circle r="70" fill="none" stroke="#23252b" stroke-width="13"/>
<circle r="60" fill="none" stroke="#d8dbe2" stroke-width="4"/>
<g id="spokesR" stroke="#cfd3da" stroke-width="2.5">
<line y1="-59" y2="59"/>
<line y1="-59" y2="59" transform="rotate(30)"/>
<line y1="-59" y2="59" transform="rotate(60)"/>
<line y1="-59" y2="59" transform="rotate(90)"/>
<line y1="-59" y2="59" transform="rotate(120)"/>
<line y1="-59" y2="59" transform="rotate(150)"/>
</g>
<circle r="7" fill="#3a3d45"/>
</g>
<!-- 前轮 -->
<g transform="translate(565 460)">
<circle r="70" fill="none" stroke="#23252b" stroke-width="13"/>
<circle r="60" fill="none" stroke="#d8dbe2" stroke-width="4"/>
<g id="spokesF" stroke="#cfd3da" stroke-width="2.5">
<line y1="-59" y2="59"/>
<line y1="-59" y2="59" transform="rotate(30)"/>
<line y1="-59" y2="59" transform="rotate(60)"/>
<line y1="-59" y2="59" transform="rotate(90)"/>
<line y1="-59" y2="59" transform="rotate(120)"/>
<line y1="-59" y2="59" transform="rotate(150)"/>
</g>
<circle r="7" fill="#3a3d45"/>
</g>
<!-- 车架 -->
<g stroke="#e2474b" stroke-width="9" stroke-linecap="round" fill="none">
<path d="M295 460 L425 462"/><!-- 后下叉 -->
<path d="M295 460 L392 352"/><!-- 后上叉 -->
<path d="M425 462 L392 352"/><!-- 座管 -->
<path d="M392 352 L540 342"/><!-- 上管 -->
<path d="M425 462 L546 360"/><!-- 下管 -->
<path d="M538 338 L552 380" stroke-width="11"/><!-- 头管 -->
<path d="M552 380 L565 460" stroke="#c23438"/><!-- 前叉 -->
</g>
<path d="M392 352 L386 336" stroke="#565b64" stroke-width="6" stroke-linecap="round"/><!-- 座杆 -->
<path d="M354 338 C 368 326, 404 326, 416 336 C 404 344, 366 344, 354 338 Z" fill="#2f3138"/><!-- 车座 -->
<path d="M540 342 L530 318" stroke="#565b64" stroke-width="8" stroke-linecap="round"/><!-- 把立 -->
<path d="M530 318 C 522 310, 512 312, 508 324" fill="none" stroke="#565b64" stroke-width="7" stroke-linecap="round"/><!-- 车把 -->
<circle cx="508" cy="324" r="4.5" fill="#33343a"/><!-- 握把 -->
<!-- 牙盘、飞轮、链条 -->
<circle cx="425" cy="462" r="23" fill="none" stroke="#8f96a1" stroke-width="6"/>
<circle cx="295" cy="460" r="12" fill="none" stroke="#8f96a1" stroke-width="4"/>
<path id="chain" d="M295 448 L425 438 A24 24 0 0 1 425 486 L295 472 A12 12 0 0 1 295 448 Z"
fill="none" stroke="#4d515a" stroke-width="5" stroke-dasharray="8 5"/>
<!-- 鹈鹕(整体随踩踏上下起伏) -->
<g id="peli">
<!-- 尾羽 -->
<path d="M300 244 L246 210 L272 246 L238 242 L268 264 L296 258 Z" fill="#e9e2c8" stroke="#d3c8a0" stroke-width="2" stroke-linejoin="round"/>
<!-- 身体 -->
<ellipse cx="350" cy="268" rx="78" ry="54" transform="rotate(-12 350 268)" fill="#f8f4e6" stroke="#d9cfa8" stroke-width="2.5"/>
<!-- 颈部与头(带轻微摆动) -->
<g id="neckhead">
<path d="M394 240 C 438 236, 420 186, 458 156" fill="none" stroke="#f8f4e6" stroke-width="24" stroke-linecap="round"/>
<path d="M394 240 C 438 236, 420 186, 458 156" fill="none" stroke="#d9cfa8" stroke-width="26" stroke-linecap="round" opacity=".25"/>
<circle cx="462" cy="148" r="22" fill="#f8f4e6" stroke="#d9cfa8" stroke-width="2"/>
<!-- 喉囊 -->
<path d="M469 158 C 482 208, 544 212, 594 170 Z" fill="#f7b04a" stroke="#de8e30" stroke-width="2" stroke-linejoin="round"/>
<!-- 上喙 -->
<path d="M466 144 C 505 143, 560 154, 596 167 L 596 172 C 558 167, 502 158, 467 159 Z" fill="#f2953f" stroke="#d97b28" stroke-width="1.5" stroke-linejoin="round"/>
<!-- 小头盔(安全第一) -->
<path d="M441 143 A 21 21 0 0 1 483 141 L 483 145 L 441 147 Z" fill="#e2474b" stroke="#c23438" stroke-width="1.5"/>
<path d="M450 129 Q 462 122 474 128" fill="none" stroke="#fff" stroke-width="3.5" stroke-linecap="round" opacity=".85"/>
<path d="M448 146 L 454 165" stroke="#c23438" stroke-width="2" stroke-linecap="round"/>
<!-- 眼睛 -->
<circle cx="470" cy="141" r="3.6" fill="#26221c"/>
<circle cx="471.3" cy="139.7" r="1.1" fill="#fff"/>
</g>
<!-- 扶车把的翅膀 -->
<path d="M396 240 C 430 244, 472 270, 502 312 L 513 321 C 516 329, 508 336, 500 332 L 489 325 C 452 300, 414 276, 390 260 Z"
fill="#efe8d2" stroke="#d9cfa8" stroke-width="2" stroke-linejoin="round"/>
<!-- 收拢的翅膀 + 黑色飞羽尖 -->
<path d="M322 278 L270 296 L316 262 Z" fill="#5b646e"/>
<path d="M310 250 C 330 226, 372 224, 398 246 C 392 268, 370 286, 336 288 C 318 282, 306 268, 310 250 Z"
fill="#efe8d2" stroke="#d9cfa8" stroke-width="2"/>
<path d="M324 252 C 344 240, 372 240, 390 250 M320 264 C 340 252, 368 252, 386 262" fill="none" stroke="#d9cfa8" stroke-width="2" stroke-linecap="round"/>
</g>
<!-- 近侧曲柄 + 腿脚(画在最前面) -->
<line id="crankN" x1="425" y1="462" x2="425" y2="502" stroke="#aab0bb" stroke-width="8" stroke-linecap="round"/>
<circle cx="425" cy="462" r="6" fill="#454a52"/>
<path id="legN" d="" fill="none" stroke="#ef9f3a" stroke-width="13" stroke-linecap="round" stroke-linejoin="round"/>
<rect id="pedN" x="-14" y="-5" width="28" height="10" rx="3" fill="#565b64"/>
<path id="footN" d="M-4 -2 C 2 -8, 14 -8, 22 -5 L 30 -1 L 22 1 L 28 4 L 19 5 L 22 10 L 8 8 C 0 7, -6 4, -4 -2 Z" fill="#f2a93f" stroke="#d18a25" stroke-width="1.5"/>
</svg>
</div>
<p class="hint">纯 SVG + 一点 JS:双腿是两段骨骼反解,脚永远踩在脚踏上</p>
<script>
(function () {
var $ = function (id) { return document.getElementById(id); };
var BB = { x: 425, y: 462 }; // 五通(曲柄中心)
var CRANK = 40; // 曲柄长度
var HIPN = { x: 374, y: 314 }; // 近侧髋关节
var HIPF = { x: 365, y: 310 }; // 远侧髋关节
var L1 = 105, L2 = 100; // 大腿 / 小腿长度
var PERIOD = 1.8; // 踩一圈的秒数
var GEAR = 2.3; // 牙盘:飞轮 传动比
var WHEEL_R = 70;
var OMEGA = 2 * Math.PI / PERIOD;
// 两段骨骼反解:已知髋和脚踝位置,求膝盖(膝盖朝前)
function knee(hip, ankle) {
var dx = ankle.x - hip.x, dy = ankle.y - hip.y;
var d = Math.hypot(dx, dy);
var dc = Math.min(Math.max(d, Math.abs(L1 - L2) + 0.5), L1 + L2 - 0.5);
var ux = dx / d, uy = dy / d;
var a = (L1 * L1 - L2 * L2 + dc * dc) / (2 * dc);
var h = Math.sqrt(Math.max(L1 * L1 - a * a, 0));
return { x: hip.x + a * ux + h * uy, y: hip.y + a * uy - h * ux };
}
function legD(hip, k, ankle) {
return 'M' + hip.x.toFixed(1) + ' ' + hip.y.toFixed(1) +
' L' + k.x.toFixed(1) + ' ' + k.y.toFixed(1) +
' L' + ankle.x.toFixed(1) + ' ' + ankle.y.toFixed(1);
}
function setCrank(crankId, pedalId, p) {
var c = $(crankId);
c.setAttribute('x2', p.x.toFixed(1));
c.setAttribute('y2', p.y.toFixed(1));
$(pedalId).setAttribute('transform', 'translate(' + p.x.toFixed(1) + ' ' + p.y.toFixed(1) + ')');
}
var dashOff = 0, chainOff = 0, last = performance.now();
function frame(now) {
var dt = Math.min((now - last) / 1000, 0.05);
last = now;
var th = now / 1000 * OMEGA; // 曲柄角度
var bob = 2.5 * Math.sin(2 * th + 0.9); // 身体起伏
// 脚踏位置(两侧相差 180°)
var pN = { x: BB.x + CRANK * Math.cos(th), y: BB.y + CRANK * Math.sin(th) };
var pF = { x: BB.x - CRANK * Math.cos(th), y: BB.y - CRANK * Math.sin(th) };
setCrank('crankN', 'pedN', pN);
setCrank('crankF', 'pedF', pF);
// 腿:髋随身体起伏,脚踝钉在脚踏上方
var hipN = { x: HIPN.x, y: HIPN.y + bob };
var hipF = { x: HIPF.x, y: HIPF.y + bob };
var aN = { x: pN.x, y: pN.y - 6 };
var aF = { x: pF.x, y: pF.y - 6 };
$('legN').setAttribute('d', legD(hipN, knee(hipN, aN), aN));
$('legF').setAttribute('d', legD(hipF, knee(hipF, aF), aF));
$('footN').setAttribute('transform', 'translate(' + aN.x.toFixed(1) + ' ' + aN.y.toFixed(1) + ') rotate(' + (8 * Math.cos(th)).toFixed(1) + ')');
$('footF').setAttribute('transform', 'translate(' + aF.x.toFixed(1) + ' ' + aF.y.toFixed(1) + ') rotate(' + (-8 * Math.cos(th)).toFixed(1) + ')');
// 车轮按传动比转动
var wdeg = th * GEAR * 180 / Math.PI;
$('spokesR').setAttribute('transform', 'rotate(' + wdeg.toFixed(1) + ')');
$('spokesF').setAttribute('transform', 'rotate(' + wdeg.toFixed(1) + ')');
// 路面虚线按轮速后退,链条按牙盘线速度循环
var speed = WHEEL_R * OMEGA * GEAR;
dashOff = (dashOff + speed * dt) % 90;
$('roadDash').setAttribute('stroke-dashoffset', dashOff.toFixed(1));
chainOff = (chainOff - OMEGA * 24 * dt) % 13;
$('chain').setAttribute('stroke-dashoffset', chainOff.toFixed(1));
// 鹈鹕起伏 + 脖子轻摆
$('peli').setAttribute('transform', 'translate(0 ' + bob.toFixed(2) + ')');
$('neckhead').setAttribute('transform', 'rotate(' + (2.4 * Math.sin(2 * th - 0.7)).toFixed(2) + ' 400 240)');
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>