感觉还行吧

svg源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>鹈鹕骑单车 · Pelican on a Bicycle (SVG 2D Animation)</title>
<style>
:root {
--bg-page: #0f172a;
--panel-bg: rgba(15, 23, 42, 0.78);
--panel-border: rgba(255, 255, 255, 0.14);
--text-main: #f8fafc;
--text-sub: #94a3b8;
--accent: #f97316;
--accent-hover: #fb923c;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
min-height: 100vh;
background: radial-gradient(circle at 50% 20%, #1e293b 0%, #090d16 100%);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text-main);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
overflow: hidden;
}
.stage-wrapper {
position: relative;
width: min(1060px, 96vw);
aspect-ratio: 16 / 10;
border-radius: 24px;
overflow: hidden;
box-shadow:
0 30px 70px -15px rgba(0, 0, 0, 0.7),
0 0 0 1px rgba(255, 255, 255, 0.12);
background: #85d7ff;
}
svg#scene {
width: 100%;
height: 100%;
display: block;
}
/* 顶部标题角标 */
.hud-header {
position: absolute;
top: 18px;
left: 22px;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
border-radius: 999px;
background: var(--panel-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--panel-border);
pointer-events: none;
}
.hud-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 10px #22c55e;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.25); opacity: 0.7; }
}
.hud-title {
font-size: 14px;
font-weight: 600;
letter-spacing: 0.4px;
}
.hud-stats {
font-size: 12px;
color: var(--text-sub);
font-variant-numeric: tabular-nums;
border-left: 1px solid rgba(255, 255, 255, 0.15);
padding-left: 12px;
}
/* 底部交互控制栏 */
.controls-bar {
position: absolute;
bottom: 18px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 14px;
padding: 10px 18px;
border-radius: 999px;
background: var(--panel-bg);
backdrop-filter: blur(14px);
border: 1px solid var(--panel-border);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
flex-wrap: wrap;
justify-content: center;
max-width: 94%;
}
.ctrl-group {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-main);
}
.ctrl-label {
color: var(--text-sub);
font-size: 12px;
font-weight: 500;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 110px;
height: 6px;
background: rgba(255, 255, 255, 0.18);
border-radius: 999px;
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent);
border: 2px solid #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
transition: transform 0.15s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.18);
}
.btn {
appearance: none;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.08);
color: var(--text-main);
font-size: 13px;
font-weight: 500;
padding: 6px 13px;
border-radius: 999px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.18s ease;
}
.btn:hover {
background: rgba(255, 255, 255, 0.18);
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: linear-gradient(135deg, #f97316, #ea580c);
border-color: rgba(255, 255, 255, 0.25);
}
.btn-primary:hover {
background: linear-gradient(135deg, #fb923c, #f97316);
}
.divider {
width: 1px;
height: 20px;
background: rgba(255, 255, 255, 0.14);
}
.speed-val {
min-width: 34px;
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: #fdba74;
}
</style>
</head>
<body>
<div class="stage-wrapper">
<!-- 顶部状态条 -->
<div class="hud-header">
<span class="hud-dot"></span>
<span class="hud-title">海滨骑行鹈鹕 · SVG 2D 实时骨骼动画</span>
<span class="hud-stats" id="hudTelemetry">时速 22 km/h · 踏频 75 RPM</span>
</div>
<!-- 主 SVG 画布 -->
<svg id="scene" viewBox="0 0 1000 625" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 天空渐变(白天/黄昏/夜晚可动态切换) -->
<linearGradient id="skyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop id="skyStop1" offset="0%" stop-color="#38bdf8" />
<stop id="skyStop2" offset="58%" stop-color="#bae6fd" />
<stop id="skyStop3" offset="100%" stop-color="#fef3c7" />
</linearGradient>
<!-- 海洋渐变 -->
<linearGradient id="seaGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop id="seaStop1" offset="0%" stop-color="#0284c7" />
<stop id="seaStop2" offset="100%" stop-color="#0369a1" />
</linearGradient>
<!-- 公路渐变 -->
<linearGradient id="roadGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#334155" />
<stop offset="100%" stop-color="#1e293b" />
</linearGradient>
<!-- 鹈鹕大喉囊渐变 -->
<linearGradient id="pouchGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fbbf24" />
<stop offset="55%" stop-color="#f59e0b" />
<stop offset="100%" stop-color="#ea580c" />
</linearGradient>
<!-- 鹈鹕上喙渐变 -->
<linearGradient id="upperBeakGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#fde047" />
<stop offset="70%" stop-color="#fb923c" />
<stop offset="100%" stop-color="#ef4444" />
</linearGradient>
<!-- 羽毛身体立体渐变 -->
<linearGradient id="bodyFeatherGrad" x1="15%" y1="5%" x2="85%" y2="95%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="70%" stop-color="#f1f5f9" />
<stop offset="100%" stop-color="#cbd5e1" />
</linearGradient>
<!-- 翅膀阴影渐变 -->
<linearGradient id="wingGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="65%" stop-color="#e2e8f0" />
<stop offset="100%" stop-color="#94a3b8" />
</linearGradient>
<!-- 车架烤漆渐变 -->
<linearGradient id="frameGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ef4444" />
<stop offset="50%" stop-color="#dc2626" />
<stop offset="100%" stop-color="#991b1b" />
</linearGradient>
<!-- 金属光泽渐变 -->
<linearGradient id="chromeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#f8fafc" />
<stop offset="50%" stop-color="#94a3b8" />
<stop offset="100%" stop-color="#475569" />
</linearGradient>
<!-- 太阳发光滤镜 -->
<filter id="sunGlow" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="14" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<!-- 柔和投影 -->
<filter id="softShadow" x="-15%" y="-15%" width="130%" height="130%">
<feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.22" />
</filter>
<!-- 单个车轮复用组件(圆心在 0,0) -->
<g id="bikeWheel">
<!-- 外胎阴影底 -->
<circle r="92" fill="none" stroke="#0f172a" stroke-width="15" />
<!-- 复古米白胎壁 -->
<circle r="83" fill="none" stroke="#fef3c7" stroke-width="5" />
<!-- 铝合金轮圈 -->
<circle r="79" fill="none" stroke="url(#chromeGrad)" stroke-width="4" />
<!-- 辐条组 (16根交织) -->
<g stroke="#cbd5e1" stroke-width="1.6" stroke- opacity="0.85">
<line x1="-78" y1="0" x2="78" y2="0" />
<line x1="0" y1="-78" x2="0" y2="78" />
<line x1="-55.1" y1="-55.1" x2="55.1" y2="55.1" />
<line x1="-55.1" y1="55.1" x2="55.1" y2="-55.1" />
<line x1="-72" y1="-29.8" x2="72" y2="29.8" />
<line x1="-29.8" y1="-72" x2="29.8" y2="72" />
<line x1="-72" y1="29.8" x2="72" y2="-29.8" />
<line x1="-29.8" y1="72" x2="29.8" y2="-72" />
</g>
<!-- 气嘴与反光片(让旋转清晰可见) -->
<rect x="-4" y="-76" width="8" height="11" rx="2" fill="#f59e0b" />
<circle cx="52" cy="30" r="5.5" fill="#fb923c" stroke="#fff" stroke-width="1.5" />
<circle cx="-52" cy="-30" r="5.5" fill="#38bdf8" stroke="#fff" stroke-width="1.5" />
</g>
</defs>
<!-- ================= 1. 背景天空与天体 ================= -->
<rect width="1000" height="470" fill="url(#skyGrad)" />
<!-- 星星层(夜晚模式显现) -->
<g id="starsLayer" opacity="0" fill="#fff">
<circle cx="110" cy="65" r="1.8" />
<circle cx="240" cy="40" r="2.2" />
<circle cx="380" cy="90" r="1.5" />
<circle cx="520" cy="50" r="2.4" />
<circle cx="670" cy="78" r="1.6" />
<circle cx="880" cy="48" r="2.1" />
<circle cx="930" cy="120" r="1.5" />
<circle cx="160" cy="145" r="1.4" />
<circle cx="760" cy="135" r="2.0" />
</g>
<!-- 太阳 / 月亮 -->
<g id="celestialBody" transform="translate(820, 110)" filter="url(#sunGlow)">
<circle id="sunHalo" r="56" fill="#fef08a" opacity="0.35" />
<circle id="sunCore" r="40" fill="#fde047" />
</g>
<!-- 远景流云层 -->
<g id="cloudsLayer" opacity="0.88" fill="#ffffff">
<g class="cloud" data-x="120" data-y="85" data-speed="0.18" data-scale="0.9">
<path d="M0,30 Q10,5 38,12 Q55,-10 85,8 Q115,2 125,26 Q142,30 138,45 L-10,45 Q-15,32 0,30 Z" />
</g>
<g class="cloud" data-x="480" data-y="55" data-speed="0.12" data-scale="1.15">
<path d="M0,30 Q10,5 38,12 Q55,-10 85,8 Q115,2 125,26 Q142,30 138,45 L-10,45 Q-15,32 0,30 Z" />
</g>
<g class="cloud" data-x="860" data-y="105" data-speed="0.22" data-scale="0.75">
<path d="M0,30 Q10,5 38,12 Q55,-10 85,8 Q115,2 125,26 Q142,30 138,45 L-10,45 Q-15,32 0,30 Z" />
</g>
</g>
<!-- 远方海鸥剪影 -->
<g id="seagullsLayer" stroke="#475569" stroke-width="2.2" fill="none" stroke-linecap="round" opacity="0.55">
<path id="gull1" d="M180,150 Q192,138 202,148 Q212,138 224,150" />
<path id="gull2" d="M235,168 Q244,159 252,166 Q260,159 269,168" />
</g>
<!-- ================= 2. 远山岛屿与海洋层 ================= -->
<!-- 远景海岛与灯塔 -->
<g id="islandLayer">
<path d="M-40,385 Q90,310 240,360 Q360,335 470,385 Z" fill="#64748b" opacity="0.38" />
<path d="M620,385 Q770,295 920,350 Q1010,330 1060,385 Z" fill="#475569" opacity="0.35" />
</g>
<!-- 蔚蓝大海 -->
<rect y="380" width="1000" height="90" fill="url(#seaGrad)" />
<!-- 滚动海浪纹与远景帆船 -->
<g id="seaWavesLayer">
<!-- 远景小帆船 -->
<g id="sailboat" transform="translate(260, 376)">
<polygon points="0,0 18,-36 22,0" fill="#f8fafc" />
<polygon points="25,0 25,-30 40,0" fill="#fed7aa" />
<path d="M-4,2 L44,2 L38,10 L2,10 Z" fill="#9a3412" />
</g>
<!-- 波浪线条 -->
<path id="waveLine1" d="M-100,402 Q-50,395 0,402 T100,402 T200,402 T300,402 T400,402 T500,402 T600,402 T700,402 T800,402 T900,402 T1000,402 T1100,402"
fill="none" stroke="#7dd3fc" stroke-width="2.5" opacity="0.55" />
<path id="waveLine2" d="M-100,425 Q-50,418 0,425 T100,425 T200,425 T300,425 T400,425 T500,425 T600,425 T700,425 T800,425 T900,425 T1000,425 T1100,425"
fill="none" stroke="#bae6fd" stroke-width="2" opacity="0.4" />
</g>
<!-- ================= 3. 海滨步道与棕榈树视差层 ================= -->
<!-- 沙滩缓冲带 -->
<path d="M0,445 Q250,438 500,445 T1000,443 L1000,480 L0,480 Z" fill="#fde68a" />
<!-- 视差移动景物(棕榈树 & 海滨里程桩) -->
<g id="sceneryLayer"></g>
<!-- 木质海滨护栏柱 -->
<g id="railingLayer"></g>
<!-- ================= 4. 柏油单车道 ================= -->
<rect y="476" width="1000" height="149" fill="url(#roadGrad)" />
<!-- 路肩石缘 -->
<rect y="472" width="1000" height="8" fill="#cbd5e1" />
<rect y="472" width="1000" height="3" fill="#f8fafc" />
<!-- 车道中线(动态滚动) -->
<g id="roadDashes" fill="#facc15" opacity="0.9"></g>
<!-- 路面细颗粒掠过线 -->
<g id="roadPebbles" stroke="#64748b" stroke-width="2" stroke-linecap="round" opacity="0.55"></g>
<!-- ================= 5. 主角:鹈鹕与自行车总成 ================= -->
<!-- 车下地面动态阴影 -->
<ellipse id="groundShadow" cx="500" cy="542" rx="235" ry="15" fill="#090d16" opacity="0.42" />
<!-- 整车与角色主容器(带动避震微颠簸与冲刺前倾) -->
<g id="rigRoot" transform="translate(0, 0)">
<!-- ===== 5.1 后景层:鹈鹕左腿(远端腿,在车架后方) ===== -->
<g id="leftLegGroup">
<!-- 左大腿 -->
<path id="leftThigh" d="" stroke="#cbd5e1" stroke-width="20" stroke-linecap="round" fill="none" />
<!-- 左小腿 -->
<path id="leftShin" d="" stroke="#d97706" stroke-width="10" stroke-linecap="round" fill="none" />
<!-- 左脚掌蹼(贴合左踏板) -->
<path id="leftFoot" d="" fill="#d97706" stroke="#b45309" stroke-width="2" stroke-linejoin="round" />
</g>
<!-- 左曲柄与左踏板(在五通后侧) -->
<g id="leftCrankGroup">
<line id="leftCrankArm" x1="455" y1="448" x2="455" y2="490" stroke="#64748b" stroke-width="8" stroke-linecap="round" />
<rect id="leftPedal" x="439" y="485" width="32" height="9" rx="4" fill="#1e293b" stroke="#475569" stroke-width="2" />
</g>
<!-- ===== 5.2 自行车车轮组 ===== -->
<!-- 后轮 (中心 305, 448) -->
<g transform="translate(305, 448)">
<g id="rearWheelSpin">
<use href="#bikeWheel" />
</g>
</g>
<!-- 前轮 (中心 675, 448) -->
<g transform="translate(675, 448)">
<g id="frontWheelSpin">
<use href="#bikeWheel" />
</g>
</g>
<!-- ===== 5.3 传动链条与飞轮 ===== -->
<g id="drivetrain">
<!-- 后飞轮 -->
<circle cx="305" cy="448" r="18" fill="#475569" stroke="#cbd5e1" stroke-width="3" />
<circle cx="305" cy="448" r="8" fill="#1e293b" />
<!-- 链条上下边(带滚动虚线) -->
<path id="chainLoop" d="M 305,430 L 455,416 A 32,32 0 1,1 455,480 L 305,466 A 18,18 0 1,1 305,430 Z"
fill="none" stroke="#1e293b" stroke-width="5.5" />
<path id="chainLinks" d="M 305,430 L 455,416 A 32,32 0 1,1 455,480 L 305,466 A 18,18 0 1,1 305,430 Z"
fill="none" stroke="#94a3b8" stroke-width="3" stroke-dasharray="6 6" />
</g>
<!-- ===== 5.4 自行车车架、挡泥板与车把 ===== -->
<g id="bikeFrame" filter="url(#softShadow)">
<!-- 后轮复古挡泥板 -->
<path d="M 206,442 A 99,99 0 0,1 388,392" fill="none" stroke="#fef3c7" stroke-width="8" stroke-linecap="round" />
<!-- 前轮复古挡泥板 -->
<path d="M 592,395 A 99,99 0 0,1 756,390" fill="none" stroke="#fef3c7" stroke-width="8" stroke-linecap="round" />
<!-- 后货架与装鱼竹篓 -->
<line x1="305" y1="448" x2="275" y2="338" stroke="#94a3b8" stroke-width="4.5" stroke-linecap="round" />
<line x1="392" y1="338" x2="255" y2="338" stroke="#cbd5e1" stroke-width="6" stroke-linecap="round" />
<!-- 车尾小竹篓与旗帜 -->
<g id="rearBasket" transform="translate(258, 294)">
<!-- 小三角破风旗 -->
<line x1="8" y1="4" x2="-12" y2="-45" stroke="#cbd5e1" stroke-width="3" />
<path id="pennantFlag" d="M-12,-45 L-58,-34 L-10,-22 Z" fill="#38bdf8" />
<!-- 藤编篮框 -->
<rect x="0" y="0" width="68" height="42" rx="7" fill="#d97706" stroke="#92400e" stroke-width="3" />
<line x1="4" y1="14" x2="64" y2="14" stroke="#92400e" stroke-width="2" />
<line x1="4" y1="28" x2="64" y2="28" stroke="#92400e" stroke-width="2" />
<!-- 探出头的小鱼尾 -->
<g id="basketFish" transform="translate(28, -4) rotate(-18)">
<path d="M0,12 Q10,-6 24,8 L36,-2 L32,12 L40,20 L24,16 Q10,24 0,12 Z" fill="#38bdf8" stroke="#0284c7" stroke-width="2" />
<circle cx="8" cy="10" r="2" fill="#0f172a" />
</g>
</g>
<!-- 座管与车座弹簧 -->
<line x1="455" y1="448" x2="380" y2="276" stroke="#cbd5e1" stroke-width="9" stroke-linecap="round" />
<!-- 头管延伸立管 -->
<path d="M 620,308 L 598,228 Q 593,212 572,212 L 542,212" fill="none" stroke="url(#chromeGrad)" stroke-width="9" stroke-linecap="round" />
<!-- 主车架经典钻石管型 -->
<g stroke="url(#frameGrad)" stroke-width="13" stroke-linejoin="round" stroke-linecap="round" fill="none">
<!-- 后下叉 -->
<line x1="305" y1="448" x2="455" y2="448" />
<!-- 后上叉 -->
<line x1="305" y1="448" x2="395" y2="310" />
<!-- 立管 -->
<line x1="455" y1="448" x2="395" y2="310" />
<!-- 下管 -->
<line x1="455" y1="448" x2="612" y2="326" />
<!-- 复古弧形上管 -->
<path d="M 395,310 Q 505,302 606,304" />
<!-- 头管 -->
<line x1="602" y1="292" x2="620" y2="338" />
<!-- 前叉 -->
<path d="M 615,332 Q 642,395 675,448" />
</g>
<!-- 车架高光装饰贴花 -->
<line x1="485" y1="422" x2="578" y2="352" stroke="#fca5a5" stroke-width="3" stroke-linecap="round" opacity="0.7" />
<!-- 前后花鼓盖 -->
<circle cx="305" cy="448" r="9" fill="#f8fafc" stroke="#334155" stroke-width="3" />
<circle cx="675" cy="448" r="9" fill="#f8fafc" stroke="#334155" stroke-width="3" />
<!-- 复古真皮车座 -->
<g id="saddleGroup" transform="translate(380, 274)">
<path d="M -38,6 Q -42,-12 -14,-10 Q 18,-6 36,-2 Q 44,4 28,12 Q -10,16 -38,6 Z"
fill="#78350f" stroke="#451a03" stroke-width="3" />
<circle cx="-26" cy="3" r="2.2" fill="#fbbf24" />
<circle cx="-16" cy="5" r="2.2" fill="#fbbf24" />
</g>
<!-- 车头复古大灯 -->
<g id="headlamp" transform="translate(626, 286)">
<path d="M 0,-12 L 18,-16 A 14,14 0 0,1 18,16 L 0,12 Z" fill="#cbd5e1" stroke="#475569" stroke-width="2.5" />
<ellipse cx="20" cy="0" rx="5" ry="14" fill="#fef08a" />
<!-- 夜间车灯光束 -->
<polygon id="headlightBeam" points="24,-10 360,-85 360,115 24,10" fill="url(#skyGrad)" opacity="0" />
</g>
<!-- 车铃铛 -->
<g id="bikeBell" transform="translate(566, 200)" style="cursor: pointer;">
<path d="M -12,6 A 12,12 0 0,1 12,6 Z" fill="#fbbf24" stroke="#b45309" stroke-width="2" />
<rect x="-3" y="6" width="6" height="6" fill="#94a3b8" />
</g>
<!-- 铃铛音波环(触发时闪烁) -->
<g id="bellWaves" transform="translate(566, 192)" opacity="0" fill="none" stroke="#fde047" stroke-width="3" stroke-linecap="round">
<path d="M -16,-8 Q 0,-22 16,-8" />
<path d="M -24,-16 Q 0,-34 24,-16" />
</g>
</g>
<!-- ===== 5.5 大齿盘与右曲柄踏板(前景侧) ===== -->
<g id="crankWheelGroup" transform="translate(455, 448)">
<g id="chainringSpin">
<circle r="32" fill="#e2e8f0" stroke="#475569" stroke-width="4" />
<circle r="24" fill="none" stroke="#94a3b8" stroke-width="2" stroke-dasharray="8 6" />
<!-- 五爪镂空 -->
<line x1="0" y1="-28" x2="0" y2="28" stroke="#475569" stroke-width="4" />
<line x1="-26" y1="-9" x2="26" y2="9" stroke="#475569" stroke-width="4" />
<line x1="-17" y1="22" x2="17" y2="-22" stroke="#475569" stroke-width="4" />
<circle r="8" fill="#334155" />
</g>
</g>
<!-- ===== 5.6 鹈鹕角色主体(随踩踏重心起伏) ===== -->
<g id="pelicanBodyRoot">
<!-- 左翼(远端翅膀,扶住远端车把) -->
<path id="leftWing" d="M 430,220 Q 495,188 552,208"
fill="none" stroke="#cbd5e1" stroke-width="18" stroke-linecap="round" />
<!-- 翘起的尾羽 -->
<g id="tailFeathers" transform="translate(342, 248)">
<path d="M 10,8 L -32,-14 Q -42,-4 -22,10 L -38,6 Q -42,18 -16,22 L -28,24 Q -24,34 8,26 Z"
fill="#f8fafc" stroke="#94a3b8" stroke-width="2.5" stroke-linejoin="round" />
</g>
<!-- 饱满圆润的鹈鹕躯干 -->
<path id="pelicanTorso"
d="M 345,252
C 332,205 375,165 426,178
C 468,188 490,235 468,268
C 446,298 362,292 345,252 Z"
fill="url(#bodyFeatherGrad)" stroke="#94a3b8" stroke-width="2.8" />
<!-- 肚子软毛纹理 -->
<path d="M 382,268 Q 418,278 446,258" fill="none" stroke="#cbd5e1" stroke-width="2.2" stroke-linecap="round" />
<path d="M 392,252 Q 422,260 448,242" fill="none" stroke="#e2e8f0" stroke-width="2" stroke-linecap="round" />
<!-- 飘扬的红白条纹运动围巾 -->
<g id="scarfGroup" transform="translate(445, 192)">
<!-- 脖圈部分 -->
<path d="M -14,-6 Q 10,8 30,-4 Q 28,14 6,16 Q -16,14 -14,-6 Z"
fill="#ef4444" stroke="#991b1b" stroke-width="2" />
<!-- 随风飘动的围巾尾带(动态路径) -->
<path id="scarfTail1" d="M -8,4 Q -65,-8 -118,8 Q -150,18 -182,4"
fill="none" stroke="#ef4444" stroke-width="14" stroke-linecap="round" />
<path id="scarfTailStripe" d="M -8,4 Q -65,-8 -118,8 Q -150,18 -182,4"
fill="none" stroke="#fef2f2" stroke-width="14" stroke-dasharray="10 14" stroke-linecap="round" />
</g>
<!-- 优雅修长的 S 型天鹅般长颈 -->
<path id="pelicanNeck"
d="M 442,196
C 426,158 430,118 458,92
C 472,78 492,82 486,110
C 480,142 465,172 468,198 Z"
fill="url(#bodyFeatherGrad)" stroke="#94a3b8" stroke-width="2.6" />
<!-- ===== 5.7 鹈鹕头部、骑行小帽、护目镜与巨大喉囊 ===== -->
<g id="pelicanHead" transform="translate(474, 88)">
<!-- 后脑勺羽冠(随风微微颤动) -->
<path id="headCrest" d="M -18,-8 L -42,-20 L -28,-2 L -46,-6 L -24,8 Z"
fill="#f8fafc" stroke="#94a3b8" stroke-width="2" stroke-linejoin="round" />
<!-- 头骨圆球 -->
<ellipse cx="0" cy="0" rx="26" ry="23" fill="#ffffff" stroke="#94a3b8" stroke-width="2.5" />
<!-- 底部弹性大喉囊(核心特征!会随颠簸和装鱼剧烈晃动) -->
<path id="gularPouch"
d="M 8,6
C 18,62 68,78 122,52
C 156,36 172,14 178,-2
L 12,2 Z"
fill="url(#pouchGrad)" stroke="#b45309" stroke-width="2.8" stroke-linejoin="round" />
<!-- 喉囊里的鼓包鱼影(喂鱼时显现) -->
<g id="pouchFishSilhouette" opacity="0" transform="translate(82, 30) rotate(12)">
<ellipse cx="0" cy="0" rx="22" ry="9" fill="#9a3412" opacity="0.35" />
</g>
<!-- 喉囊褶皱纹理线 -->
<path id="pouchCrease1" d="M 26,14 Q 68,52 134,26" fill="none" stroke="#d97706" stroke-width="2" stroke-linecap="round" opacity="0.75" />
<path id="pouchCrease2" d="M 42,10 Q 82,36 148,14" fill="none" stroke="#d97706" stroke-width="1.6" stroke-linecap="round" opacity="0.6" />
<!-- 超长上喙与粉红钩尖 -->
<path id="upperBeak"
d="M 16,-11
C 66,-16 132,-15 178,-10
C 188,-9 192,2 184,8
C 178,12 174,4 170,1
L 8,5 Z"
fill="url(#upperBeakGrad)" stroke="#c2410c" stroke-width="2.5" stroke-linejoin="round" />
<!-- 眼睛与眼周黄色裸皮 -->
<circle cx="8" cy="-5" r="9.5" fill="#fde047" />
<g id="eyeOpen">
<circle cx="8" cy="-5" r="6.5" fill="#ffffff" stroke="#475569" stroke-width="1.5" />
<circle cx="10" cy="-5" r="3.6" fill="#0f172a" />
<circle cx="11.5" cy="-6.8" r="1.5" fill="#ffffff" />
</g>
<!-- 眨眼闭合线 -->
<path id="eyeBlink" d="M 1,-5 Q 8,-1 15,-5" fill="none" stroke="#334155" stroke-width="2.5" stroke-linecap="round" opacity="0" />
<!-- 复古飞行员护目镜(推在额头上) -->
<g transform="translate(2, -18) rotate(-14)">
<path d="M -24,2 Q -2,6 18,0" fill="none" stroke="#334155" stroke-width="4" />
<rect x="2" y="-8" width="18" height="13" rx="5" fill="#38bdf8" stroke="#78350f" stroke-width="3" />
<line x1="6" y1="-5" x2="12" y2="2" stroke="#e0f2fe" stroke-width="2" stroke-linecap="round" />
</g>
<!-- 帅气蓝色条纹骑行小歪帽 (Casquette) -->
<g transform="translate(-4, -22) rotate(-10)">
<path d="M -22,4 C -22,-16 18,-18 22,2 Z" fill="#0284c7" stroke="#0369a1" stroke-width="2.2" />
<path d="M -2,-11 C 4,-11 8,-4 8,2 L -4,2 Z" fill="#facc15" />
<!-- 翘起的帽檐 -->
<path d="M 14,1 Q 32,-6 36,2 Q 26,7 12,4 Z" fill="#0369a1" />
<circle cx="-1" cy="-12" r="3" fill="#facc15" />
</g>
</g>
</g>
<!-- ===== 5.8 前景层:鹈鹕右腿(近端腿,完整逆向运动学踩踏) ===== -->
<g id="rightLegGroup">
<!-- 大腿羽毛蓬松根部 -->
<circle id="rightHipJoint" cx="406" cy="264" r="18" fill="#f1f5f9" />
<!-- 右大腿(白羽包裹上段 + 橙色脚胫) -->
<path id="rightThighFeather" d="" stroke="#f8fafc" stroke-width="24" stroke-linecap="round" fill="none" />
<path id="rightThigh" d="" stroke="#f59e0b" stroke-width="11" stroke-linecap="round" fill="none" />
<!-- 膝关节圆点 -->
<circle id="rightKneeJoint" cx="460" cy="350" r="6.5" fill="#fbbf24" stroke="#d97706" stroke-width="2" />
<!-- 右小腿 -->
<path id="rightShin" d="" stroke="#f59e0b" stroke-width="10" stroke-linecap="round" fill="none" />
<!-- 大扁平鸭蹼脚掌(踩在右踏板上) -->
<path id="rightFoot" d="" fill="#fb923c" stroke="#c2410c" stroke-width="2.4" stroke-linejoin="round" />
</g>
<!-- 右曲柄与右踏板(最前景) -->
<g id="rightCrankGroup">
<line id="rightCrankArm" x1="455" y1="448" x2="455" y2="406" stroke="url(#chromeGrad)" stroke-width="8.5" stroke-linecap="round" />
<circle cx="455" cy="448" r="6" fill="#1e293b" />
<g id="rightPedal" transform="translate(455, 406)">
<rect x="-18" y="-5" width="36" height="10" rx="4" fill="#0f172a" stroke="#cbd5e1" stroke-width="2" />
<rect x="-11" y="-2" width="8" height="4" rx="1" fill="#f97316" />
<rect x="3" y="-2" width="8" height="4" rx="1" fill="#f97316" />
</g>
</g>
<!-- ===== 5.9 前景层:鹈鹕右翅膀(伸出握住近端车把) ===== -->
<g id="rightWingGroup">
<!-- 主翼臂 -->
<path id="rightWingShape"
d="M 418,215
C 405,248 450,264 495,245
C 522,233 536,222 548,214
C 535,204 510,214 484,218
C 456,222 435,202 418,215 Z"
fill="url(#wingGrad)" stroke="#64748b" stroke-width="2.6" stroke-linejoin="round" />
<!-- 飞羽层次分界线 -->
<path id="rightWingDetail"
d="M 432,238 Q 468,244 504,232 M 448,248 Q 482,250 518,234"
fill="none" stroke="#94a3b8" stroke-width="2" stroke-linecap="round" />
<!-- 握住车把的翅尖羽毛指 -->
<ellipse cx="545" cy="213" rx="11" ry="8" transform="rotate(-12 545 213)"
fill="#ffffff" stroke="#64748b" stroke-width="2.4" />
<!-- 近端车把胶套 -->
<rect x="530" y="207" width="24" height="10" rx="5" fill="#78350f" stroke="#451a03" stroke-width="2" />
</g>
</g>
<!-- ================= 6. 投喂飞鱼与前景速度线特效层 ================= -->
<!-- 投喂抛物线飞鱼 -->
<g id="flyingFish" opacity="0" transform="translate(900, 200)">
<path d="M -22,0 C -10,-14 14,-12 24,0 L 38,-10 L 33,0 L 38,10 L 24,0 C 14,12 -10,14 -22,0 Z"
fill="#38bdf8" stroke="#0369a1" stroke-width="2.2" stroke-linejoin="round" />
<path d="M -6,-8 Q 4,0 -6,8" fill="none" stroke="#bae6fd" stroke-width="2" />
<circle cx="-12" cy="-2" r="2.3" fill="#0f172a" />
</g>
<!-- 迎面气流速度线 -->
<g id="windLinesLayer" stroke="#ffffff" stroke-linecap="round" opacity="0.45"></g>
</svg>
<!-- 底部交互控制栏 -->
<div class="controls-bar">
<div class="ctrl-group">
<span class="ctrl-label">骑行速度</span>
<input type="range" id="speedSlider" min="0.2" max="2.8" step="0.1" value="1.2" />
<span class="speed-val" id="speedLabel">1.2x</span>
</div>
<div class="divider"></div>
<button class="btn btn-primary" id="feedFishBtn" title="丢一条鲜鱼进鹈鹕的大喉囊">
🐟 投喂海鱼
</button>
<button class="btn" id="ringBellBtn" title="按响清脆的单车铃铛">
🔔 叮铃铃
</button>
<button class="btn" id="sprintBtn" title="短暂全力冲刺">
⚡ 破风冲刺
</button>
<button class="btn" id="themeBtn" title="切换白昼、黄昏、星夜">
🌅 切换时段
</button>
</div>
</div>
<script>
(() => {
// ==================== 1. 场景初始化:生成背景棕榈树、护栏、车道线 ====================
const sceneryLayer = document.getElementById('sceneryLayer');
const railingLayer = document.getElementById('railingLayer');
const roadDashes = document.getElementById('roadDashes');
const roadPebbles = document.getElementById('roadPebbles');
const windLinesLayer = document.getElementById('windLinesLayer');
const SVG_NS = 'http://www.w3.org/2000/svg';
// 创建棕榈树与海滨灌木
const palmTrees = [
{ x: 140, scale: 0.92, lean: -8 },
{ x: 490, scale: 0.76, lean: 6 },
{ x: 850, scale: 1.02, lean: -5 }
];
palmTrees.forEach((tree) => {
const g = document.createElementNS(SVG_NS, 'g');
g.innerHTML = `
<g transform="scale(${tree.scale})">
<!-- 地面树影 -->
<ellipse cx="0" cy="0" rx="36" ry="6" fill="#d97706" opacity="0.28" />
<!-- 弯曲树干 -->
<path d="M -6,0 Q ${tree.lean * 2},-75 ${tree.lean * 3},-145 L ${tree.lean * 3 + 7},-145 Q ${tree.lean * 2 + 8},-75 6,0 Z"
fill="#92400e" />
<!-- 椰子 -->
<circle cx="${tree.lean * 3 - 4}" cy="-142" r="5.5" fill="#451a03" />
<circle cx="${tree.lean * 3 + 5}" cy="-140" r="5.5" fill="#451a03" />
<!-- 棕榈树叶冠 -->
<g transform="translate(${tree.lean * 3 + 2}, -146)" fill="#15803d">
<path d="M 0,0 Q -42,-24 -76,8 Q -38,-4 0,0 Z" />
<path d="M 0,0 Q -36,-45 -58,-28 Q -28,-22 0,0 Z" fill="#16a34a" />
<path d="M 0,0 Q 38,-42 66,-18 Q 30,-18 0,0 Z" fill="#16a34a" />
<path d="M 0,0 Q 46,-18 78,16 Q 38,0 0,0 Z" />
<path d="M 0,0 Q 2,-48 18,-56 Q 14,-24 0,0 Z" fill="#22c55e" />
</g>
</g>
`;
sceneryLayer.appendChild(g);
tree.el = g;
});
// 创建海滨木栏杆 (12根循环)
const railPosts = [];
for (let i = 0; i < 12; i++) {
const g = document.createElementNS(SVG_NS, 'g');
const x = i * 95;
g.innerHTML = `
<rect x="-5" y="432" width="10" height="40" rx="2" fill="#78350f" />
<rect x="-5" y="432" width="10" height="6" rx="1" fill="#f8fafc" />
<line x1="0" y1="446" x2="95" y2="446" stroke="#d97706" stroke-width="3" />
`;
railingLayer.appendChild(g);
railPosts.push({ x, el: g });
}
// 创建公路虚线 (9段循环)
const dashes = [];
for (let i = 0; i < 9; i++) {
const rect = document.createElementNS(SVG_NS, 'rect');
rect.setAttribute('y', '546');
rect.setAttribute('width', '68');
rect.setAttribute('height', '8');
rect.setAttribute('rx', '4');
roadDashes.appendChild(rect);
dashes.push({ x: i * 130, el: rect });
}
// 创建路面掠过碎石线
const pebbles = [];
for (let i = 0; i < 14; i++) {
const line = document.createElementNS(SVG_NS, 'line');
const y = 492 + (i % 5) * 24 + Math.random() * 8;
const len = 8 + (i % 3) * 7;
line.dataset.y = y;
line.dataset.len = len;
roadPebbles.appendChild(line);
pebbles.push({ x: i * 82, y, len, el: line });
}
// 创建空中气流线
const windStreaks = [];
for (let i = 0; i < 6; i++) {
const line = document.createElementNS(SVG_NS, 'line');
line.setAttribute('stroke-width', '2');
windLinesLayer.appendChild(line);
windStreaks.push({
x: Math.random() * 1000,
y: 120 + i * 58,
len: 45 + (i % 3) * 28,
el: line
});
}
// ==================== 2. 逆向运动学 (2-Bone IK) 求解器 ====================
/**
* 根据髋关节 (hx, hy) 与脚踏点 (px, py),计算膝盖坐标 (kx, ky)
* 自行车朝右骑行,膝盖始终朝右前方弯曲
*/
function solveKneeIK(hx, hy, px, py, L1, L2) {
const dx = px - hx;
const dy = py - hy;
let d = Math.hypot(dx, dy);
const maxReach = L1 + L2 - 0.5;
const minReach = Math.abs(L1 - L2) + 0.5;
d = Math.max(minReach, Math.min(maxReach, d));
const a = (L1 * L1 - L2 * L2 + d * d) / (2 * d);
const h = Math.sqrt(Math.max(0, L1 * L1 - a * a));
const ux = dx / d;
const uy = dy / d;
// 朝右前方突出的法向量 (uy, -ux)
const kx = hx + a * ux + h * uy;
const ky = hy + a * uy - h * ux;
return { x: kx, y: ky };
}
// 生成鸭蹼脚掌 SVG Path(脚踝在踝点,脚尖朝右前方踩住踏板)
function buildWebbedFootPath(ankleX, ankleY, pedalX, pedalY, angleDeg) {
const rad = (angleDeg * Math.PI) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
const rot = (lx, ly) => {
const rx = pedalX + lx * cos - ly * sin;
const ry = pedalY + lx * sin + ly * cos;
return `${rx.toFixed(1)},${ry.toFixed(1)}`;
};
return `M ${ankleX.toFixed(1)},${ankleY.toFixed(1)} ` +
`L ${rot(-16, -2)} ` +
`L ${rot(-12, 5)} ` +
`L ${rot(24, 5)} ` +
`Q ${rot(32, -2)} ${rot(22, -7)} ` +
`L ${ankleX.toFixed(1)},${ankleY.toFixed(1)} Z`;
}
// ==================== 3. 获取动画 DOM 节点引用 ====================
const rearWheelSpin = document.getElementById('rearWheelSpin');
const frontWheelSpin = document.getElementById('frontWheelSpin');
const chainringSpin = document.getElementById('chainringSpin');
const chainLinks = document.getElementById('chainLinks');
const rightCrankArm = document.getElementById('rightCrankArm');
const rightPedal = document.getElementById('rightPedal');
const leftCrankArm = document.getElementById('leftCrankArm');
const leftPedal = document.getElementById('leftPedal');
const rightHipJoint = document.getElementById('rightHipJoint');
const rightThighFeather = document.getElementById('rightThighFeather');
const rightThigh = document.getElementById('rightThigh');
const rightKneeJoint = document.getElementById('rightKneeJoint');
const rightShin = document.getElementById('rightShin');
const rightFoot = document.getElementById('rightFoot');
const leftThigh = document.getElementById('leftThigh');
const leftShin = document.getElementById('leftShin');
const leftFoot = document.getElementById('leftFoot');
const pelicanBodyRoot = document.getElementById('pelicanBodyRoot');
const pelicanHead = document.getElementById('pelicanHead');
const gularPouch = document.getElementById('gularPouch');
const pouchCrease1 = document.getElementById('pouchCrease1');
const pouchCrease2 = document.getElementById('pouchCrease2');
const upperBeak = document.getElementById('upperBeak');
const eyeOpen = document.getElementById('eyeOpen');
const eyeBlink = document.getElementById('eyeBlink');
const pouchFishSilhouette = document.getElementById('pouchFishSilhouette');
const scarfTail1 = document.getElementById('scarfTail1');
const scarfTailStripe = document.getElementById('scarfTailStripe');
const pennantFlag = document.getElementById('pennantFlag');
const rigRoot = document.getElementById('rigRoot');
const groundShadow = document.getElementById('groundShadow');
const waveLine1 = document.getElementById('waveLine1');
const waveLine2 = document.getElementById('waveLine2');
const sailboat = document.getElementById('sailboat');
const clouds = Array.from(document.querySelectorAll('.cloud'));
const flyingFish = document.getElementById('flyingFish');
const bellWaves = document.getElementById('bellWaves');
const hudTelemetry = document.getElementById('hudTelemetry');
// ==================== 4. 状态与交互参数 ====================
let baseSpeed = 1.2;
let sprintBoost = 0;
let crankAngle = 0; // 弧度
let distanceScroll = 0;
let timeSec = 0;
// 投喂鱼状态
let fishState = {
active: false,
progress: 0,
chewTimer: 0
};
// 车铃状态
let bellTimer = 0;
// 昼夜主题列表
const themes = [
{
name: '晴朗白昼',
sky: ['#38bdf8', '#bae6fd', '#fef3c7'],
sea: ['#0284c7', '#0369a1'],
sunColor: '#fde047',
starsOpacity: 0,
beamOpacity: 0
},
{
name: '落日黄金海岸',
sky: ['#4c1d95', '#f97316', '#fde047'],
sea: ['#1e3a8a', '#7c2d12'],
sunColor: '#fb923c',
starsOpacity: 0.35,
beamOpacity: 0.15
},
{
name: '星夜巡航',
sky: ['#020617', '#0f172a', '#1e293b'],
sea: ['#090d16', '#0f172a'],
sunColor: '#f8fafc',
starsOpacity: 0.95,
beamOpacity: 0.35
}
];
let currentThemeIdx = 0;
// Web Audio 合成音效(车铃声 + 吞鱼咕噜声,无需外部音频文件)
let audioCtx = null;
function ensureAudio() {
if (!audioCtx) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (AudioContextClass) audioCtx = new AudioContextClass();
}
}
function playBellSound() {
ensureAudio();
if (!audioCtx) return;
const now = audioCtx.currentTime;
[1568, 2093].forEach((freq, idx) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + idx * 0.07);
gain.gain.setValueAtTime(0.18, now + idx * 0.07);
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.07 + 0.55);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now + idx * 0.07);
osc.stop(now + idx * 0.07 + 0.56);
});
}
function playGulpSound() {
ensureAudio();
if (!audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(240, now);
osc.frequency.exponentialRampToValueAtTime(520, now + 0.14);
osc.frequency.exponentialRampToValueAtTime(180, now + 0.28);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.31);
}
// ==================== 5. 绑定 UI 控件事件 ====================
const speedSlider = document.getElementById('speedSlider');
const speedLabel = document.getElementById('speedLabel');
speedSlider.addEventListener('input', (e) => {
baseSpeed = parseFloat(e.target.value);
speedLabel.textContent = `${baseSpeed.toFixed(1)}x`;
});
function triggerFeedFish() {
if (fishState.active) return;
fishState.active = true;
fishState.progress = 0;
}
function triggerRingBell() {
bellTimer = 0.65;
playBellSound();
}
document.getElementById('feedFishBtn').addEventListener('click', triggerFeedFish);
document.getElementById('ringBellBtn').addEventListener('click', triggerRingBell);
document.getElementById('bikeBell').addEventListener('click', triggerRingBell);
document.getElementById('sprintBtn').addEventListener('click', () => {
sprintBoost = 1.65;
triggerRingBell();
});
document.getElementById('themeBtn').addEventListener('click', () => {
currentThemeIdx = (currentThemeIdx + 1) % themes.length;
const t = themes[currentThemeIdx];
document.getElementById('skyStop1').setAttribute('stop-color', t.sky[0]);
document.getElementById('skyStop2').setAttribute('stop-color', t.sky[1]);
document.getElementById('skyStop3').setAttribute('stop-color', t.sky[2]);
document.getElementById('seaStop1').setAttribute('stop-color', t.sea[0]);
document.getElementById('seaStop2').setAttribute('stop-color', t.sea[1]);
document.getElementById('sunCore').setAttribute('fill', t.sunColor);
document.getElementById('starsLayer').setAttribute('opacity', t.starsOpacity);
document.getElementById('headlightBeam').setAttribute('opacity', t.beamOpacity);
});
// ==================== 6. 核心帧循环 (requestAnimationFrame) ====================
const BB_X = 455; // 五通中心 X
const BB_Y = 448; // 五通中心 Y
const CRANK_R = 42; // 曲柄半径
const L_THIGH = 98; // 大腿骨骼长度
const L_SHIN = 102; // 小腿骨骼长度
let lastTime = performance.now();
function animate(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
// 冲刺衰减
if (sprintBoost > 0) {
sprintBoost = Math.max(0, sprintBoost - dt * 0.7);
}
const effectiveSpeed = baseSpeed + sprintBoost;
timeSec += dt;
crankAngle += dt * effectiveSpeed * 5.2;
distanceScroll += dt * effectiveSpeed * 290;
// 更新顶部遥测数据
const kmh = Math.round(effectiveSpeed * 18.5);
const rpm = Math.round(effectiveSpeed * 62);
hudTelemetry.textContent = `时速 ${kmh} km/h · 踏频 ${rpm} RPM`;
// ---------- 6.1 车轮、牙盘、链条转动 ----------
const wheelDeg = (crankAngle * 145) % 360;
const crankDeg = ((crankAngle * 180) / Math.PI) % 360;
rearWheelSpin.setAttribute('transform', `rotate(${wheelDeg.toFixed(1)})`);
frontWheelSpin.setAttribute('transform', `rotate(${wheelDeg.toFixed(1)})`);
chainringSpin.setAttribute('transform', `rotate(${crankDeg.toFixed(1)})`);
chainLinks.setAttribute('stroke-dashoffset', (-distanceScroll * 0.35).toFixed(1));
// ---------- 6.2 左右曲柄与脚踏位置计算 ----------
const rPedalX = BB_X + Math.cos(crankAngle) * CRANK_R;
const rPedalY = BB_Y + Math.sin(crankAngle) * CRANK_R;
const lPedalX = BB_X + Math.cos(crankAngle + Math.PI) * CRANK_R;
const lPedalY = BB_Y + Math.sin(crankAngle + Math.PI) * CRANK_R;
rightCrankArm.setAttribute('x2', rPedalX.toFixed(1));
rightCrankArm.setAttribute('y2', rPedalY.toFixed(1));
rightPedal.setAttribute('transform', `translate(${rPedalX.toFixed(1)}, ${rPedalY.toFixed(1)})`);
leftCrankArm.setAttribute('x2', lPedalX.toFixed(1));
leftCrankArm.setAttribute('y2', lPedalY.toFixed(1));
leftPedal.setAttribute('x', (lPedalX - 16).toFixed(1));
leftPedal.setAttribute('y', (lPedalY - 4.5).toFixed(1));
// ---------- 6.3 身体随踩踏起伏与前倾 ----------
const bodyBobY = Math.sin(crankAngle * 2) * 5.5;
const bodyPitch = Math.cos(crankAngle * 2) * 1.4 + sprintBoost * 2.5;
pelicanBodyRoot.setAttribute(
'transform',
`translate(0, ${bodyBobY.toFixed(2)}) rotate(${bodyPitch.toFixed(2)}, 410, 250)`
);
// 整车轻颠簸
const bikeBounceY = Math.sin(crankAngle * 2 + 0.5) * 1.8;
rigRoot.setAttribute('transform', `translate(0, ${bikeBounceY.toFixed(2)})`);
groundShadow.setAttribute('rx', (235 + bikeBounceY * 2).toFixed(1));
// ---------- 6.4 右腿 & 左腿 2D IK 骨骼解算 ----------
const hipX = 406;
const hipY = 264 + bodyBobY;
rightHipJoint.setAttribute('cx', hipX.toFixed(1));
rightHipJoint.setAttribute('cy', hipY.toFixed(1));
// 右腿踝关节略高于踏板表面
const rAnkleX = rPedalX - 4;
const rAnkleY = rPedalY - 7;
const rKnee = solveKneeIK(hipX, hipY, rAnkleX, rAnkleY, L_THIGH, L_SHIN);
// 大腿上段白羽包裹(从髋部延伸到60%大腿处)
const featherEndX = hipX + (rKnee.x - hipX) * 0.58;
const featherEndY = hipY + (rKnee.y - hipY) * 0.58;
rightThighFeather.setAttribute('d', `M ${hipX.toFixed(1)},${hipY.toFixed(1)} L ${featherEndX.toFixed(1)},${featherEndY.toFixed(1)}`);
rightThigh.setAttribute('d', `M ${hipX.toFixed(1)},${hipY.toFixed(1)} L ${rKnee.x.toFixed(1)},${rKnee.y.toFixed(1)}`);
rightKneeJoint.setAttribute('cx', rKnee.x.toFixed(1));
rightKneeJoint.setAttribute('cy', rKnee.y.toFixed(1));
rightShin.setAttribute('d', `M ${rKnee.x.toFixed(1)},${rKnee.y.toFixed(1)} L ${rAnkleX.toFixed(1)},${rAnkleY.toFixed(1)}`);
const rFootTilt = Math.sin(crankAngle) * 10;
rightFoot.setAttribute('d', buildWebbedFootPath(rAnkleX, rAnkleY, rPedalX, rPedalY - 2, rFootTilt));
// 左腿(远端腿)IK
const lHipX = 414;
const lHipY = 260 + bodyBobY;
const lAnkleX = lPedalX - 4;
const lAnkleY = lPedalY - 7;
const lKnee = solveKneeIK(lHipX, lHipY, lAnkleX, lAnkleY, L_THIGH, L_SHIN);
leftThigh.setAttribute('d', `M ${lHipX.toFixed(1)},${lHipY.toFixed(1)} L ${lKnee.x.toFixed(1)},${lKnee.y.toFixed(1)}`);
leftShin.setAttribute('d', `M ${lKnee.x.toFixed(1)},${lKnee.y.toFixed(1)} L ${lAnkleX.toFixed(1)},${lAnkleY.toFixed(1)}`);
const lFootTilt = Math.sin(crankAngle + Math.PI) * 10;
leftFoot.setAttribute('d', buildWebbedFootPath(lAnkleX, lAnkleY, lPedalX, lPedalY - 2, lFootTilt));
// ---------- 6.5 鹈鹕大喉囊弹性物理摆动与咀嚼 ----------
let chewStretch = 0;
if (fishState.chewTimer > 0) {
fishState.chewTimer = Math.max(0, fishState.chewTimer - dt);
chewStretch = Math.sin(fishState.chewTimer * 28) * 18 + 14;
pouchFishSilhouette.setAttribute('opacity', Math.min(1, fishState.chewTimer * 1.2).toFixed(2));
} else {
pouchFishSilhouette.setAttribute('opacity', '0');
}
const pouchSwing = Math.sin(crankAngle * 2 - 0.8) * 7 + chewStretch;
const pouchWave = Math.cos(timeSec * 8) * 4;
const pMidY = 78 + pouchSwing * 1.15;
const pRightY = 52 + pouchSwing * 0.7 + pouchWave;
gularPouch.setAttribute(
'd',
`M 8,6 C 18,${(62 + pouchSwing * 0.8).toFixed(1)} 68,${pMidY.toFixed(1)} 122,${pRightY.toFixed(1)} C 156,${(36 + pouchSwing * 0.3).toFixed(1)} 172,14 178,-2 L 12,2 Z`
);
pouchCrease1.setAttribute('d', `M 26,14 Q 68,${(52 + pouchSwing * 0.85).toFixed(1)} 134,${(26 + pouchSwing * 0.4).toFixed(1)}`);
pouchCrease2.setAttribute('d', `M 42,10 Q 82,${(36 + pouchSwing * 0.6).toFixed(1)} 148,14`);
// 头部微点动与张嘴接鱼
const headNod = Math.sin(crankAngle * 2 - 0.4) * 3.2;
pelicanHead.setAttribute('transform', `translate(474, 88) rotate(${headNod.toFixed(2)})`);
// 鱼接近时张开大上喙
let beakOpenDeg = 0;
if (fishState.active && fishState.progress > 0.55) {
beakOpenDeg = -18 * Math.sin(((fishState.progress - 0.55) / 0.45) * Math.PI);
} else if (fishState.chewTimer > 0) {
beakOpenDeg = -Math.abs(Math.sin(fishState.chewTimer * 28)) * 8;
}
upperBeak.setAttribute('transform', `rotate(${beakOpenDeg.toFixed(1)}, 16, -5)`);
// 自然眨眼控制
const blinkCycle = timeSec % 4.2;
const isBlinking = blinkCycle > 4.02;
eyeOpen.setAttribute('opacity', isBlinking ? '0' : '1');
eyeBlink.setAttribute('opacity', isBlinking ? '1' : '0');
// ---------- 6.6 飘带围巾与小旗波动 ----------
const w1 = Math.sin(timeSec * 12) * 14;
const w2 = Math.cos(timeSec * 12 - 1.2) * 18;
const scarfPath = `M -8,4 Q -65,${(-8 + w1).toFixed(1)} -118,${(8 - w1 * 0.7).toFixed(1)} Q -150,${(14 + w2).toFixed(1)} -188,${(2 - w2).toFixed(1)}`;
scarfTail1.setAttribute('d', scarfPath);
scarfTailStripe.setAttribute('d', scarfPath);
const flagWave = Math.sin(timeSec * 16) * 6;
pennantFlag.setAttribute('d', `M-12,-45 Q-35,${(-42 + flagWave).toFixed(1)} -60,${(-33 - flagWave).toFixed(1)} L-10,-22 Z`);
// ---------- 6.7 投喂海鱼抛物线动画 ----------
if (fishState.active) {
fishState.progress += dt * 1.45;
const p = Math.min(1, fishState.progress);
// 起点在画面右侧 (940, 220),终点在鹈鹕大嘴里 (595, 92)
const fx = 940 + (595 - 940) * p;
const fy = 220 + (92 - 220) * p - Math.sin(p * Math.PI) * 145;
const fRot = -155 + p * 210;
flyingFish.setAttribute('opacity', '1');
flyingFish.setAttribute('transform', `translate(${fx.toFixed(1)}, ${fy.toFixed(1)}) rotate(${fRot.toFixed(1)})`);
if (p >= 1) {
fishState.active = false;
fishState.chewTimer = 1.1;
flyingFish.setAttribute('opacity', '0');
playGulpSound();
}
}
// ---------- 6.8 车铃音波闪烁 ----------
if (bellTimer > 0) {
bellTimer = Math.max(0, bellTimer - dt);
const waveScale = 1 + (0.65 - bellTimer) * 0.6;
bellWaves.setAttribute('opacity', (bellTimer / 0.65).toFixed(2));
bellWaves.setAttribute('transform', `translate(566, 192) scale(${waveScale.toFixed(2)})`);
} else {
bellWaves.setAttribute('opacity', '0');
}
// ---------- 6.9 视差背景滚动(流云、海浪、棕榈树、栏杆、路面、风线) ----------
// 云朵缓慢移动
clouds.forEach((c) => {
let cx = parseFloat(c.dataset.x) - dt * effectiveSpeed * 28 * parseFloat(c.dataset.speed);
if (cx < -180) cx = 1080;
c.dataset.x = cx;
c.setAttribute('transform', `translate(${cx.toFixed(1)}, ${c.dataset.y}) scale(${c.dataset.scale})`);
});
// 海面波纹与小帆船微晃
const waveShift = (distanceScroll * 0.12) % 100;
waveLine1.setAttribute('transform', `translate(${-waveShift.toFixed(1)}, 0)`);
waveLine2.setAttribute('transform', `translate(${(-waveShift * 0.7).toFixed(1)}, 0)`);
const boatBob = Math.sin(timeSec * 2.5) * 2.5;
sailboat.setAttribute('transform', `translate(260, ${(376 + boatBob).toFixed(1)}) rotate(${(boatBob * 1.2).toFixed(1)})`);
// 棕榈树中景视差
palmTrees.forEach((tree) => {
tree.x -= dt * effectiveSpeed * 125;
if (tree.x < -120) tree.x += 1220;
tree.el.setAttribute('transform', `translate(${tree.x.toFixed(1)}, 452)`);
});
// 步道木栏杆
railPosts.forEach((post) => {
post.x -= dt * effectiveSpeed * 210;
if (post.x < -95) post.x += 12 * 95;
post.el.setAttribute('transform', `translate(${post.x.toFixed(1)}, 0)`);
});
// 公路虚线高速掠过
dashes.forEach((d) => {
d.x -= dt * effectiveSpeed * 320;
if (d.x < -90) d.x += 9 * 130;
d.el.setAttribute('x', d.x.toFixed(1));
});
// 路面碎石细线
pebbles.forEach((p) => {
p.x -= dt * effectiveSpeed * 320;
if (p.x < -40) p.x += 1140;
p.el.setAttribute('x1', p.x.toFixed(1));
p.el.setAttribute('y1', p.y.toFixed(1));
p.el.setAttribute('x2', (p.x + p.len).toFixed(1));
p.el.setAttribute('y2', p.y.toFixed(1));
});
// 迎面风速线
windLinesLayer.setAttribute('opacity', Math.min(0.75, effectiveSpeed * 0.28).toFixed(2));
windStreaks.forEach((w) => {
w.x -= dt * effectiveSpeed * 520;
if (w.x < -120) {
w.x = 1060 + Math.random() * 200;
w.y = 95 + Math.random() * 280;
}
w.el.setAttribute('x1', w.x.toFixed(1));
w.el.setAttribute('y1', w.y.toFixed(1));
w.el.setAttribute('x2', (w.x + w.len * Math.min(1.8, effectiveSpeed)).toFixed(1));
w.el.setAttribute('y2', w.y.toFixed(1));
});
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
})();
</script>
</body>
</html>
感觉效果有点屌啊
经典哈基米风格,不过结构和细节好了很多
正价api吗还是ultra
看着可以啊,比上一代好多了
ultra吗?好像挺不错的
@nikonikoni #3 正价aistudio api(用的gemini pro gcp 10刀赠金),搭配cc使用high effort
物理状态看着还不错,但是缺乏艺术风格
看起来还挺不错的
@gyo #0 提示词是简单的那一句话吗
宛如核弹爆炸