Opus5.5 鹈鹕测试(你绝对想不到的细节,有点东西)

隔壁老陈 2026-09-23 01:50 1

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>鹈鹕骑车 · 黄昏栈桥</title>
<style>
:root{
--sky-top:#2b2152;
--sea-deep:#2b2c60;
box-sizing:border-box;
padding-top:env(safe-area-inset-top,0px);
padding-bottom:env(safe-area-inset-bottom,0px);
background:var(--sky-top);
}
html,body{height:100%;margin:0}
html{scroll-padding-top:env(safe-area-inset-top,0px)}
body{overflow:hidden;background:linear-gradient(var(--sky-top) 0 50%, var(--sea-deep) 50% 100%)}
#scene{display:block;width:100%;height:100%}
body::after{content:"";position:fixed;inset:0;pointer-events:none;
background:radial-gradient(ellipse at 50% 50%, rgba(0,0,0,0) 55%, rgba(22,12,42,.5) 100%)}
.tw{animation:tw 3s ease-in-out infinite}
@keyframes tw{0%,100%{opacity:.15}50%{opacity:.9}}
</style>
</head>
<body>
<svg id="scene" viewBox="0 0 1200 700" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="黄昏的海边栈桥上,一只鹈鹕骑着自行车">
<defs>
<linearGradient id="gSky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2b2152"/>
<stop offset=".32" stop-color="#5e3b78"/>
<stop offset=".6" stop-color="#c9607e"/>
<stop offset=".82" stop-color="#f7955f"/>
<stop offset="1" stop-color="#ffd48e"/>
</linearGradient>
<radialGradient id="gGlow">
<stop offset="0" stop-color="#fff2c0" stop-opacity=".95"/>
<stop offset=".22" stop-color="#ffd07a" stop-opacity=".6"/>
<stop offset=".55" stop-color="#ff9a6a" stop-opacity=".18"/>
<stop offset="1" stop-color="#ff8a6a" stop-opacity="0"/>
</radialGradient>
<linearGradient id="gSun" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fff6d2"/>
<stop offset="1" stop-color="#ffb457"/>
</linearGradient>
<linearGradient id="gSea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f7a47c"/>
<stop offset=".15" stop-color="#dc7680"/>
<stop offset=".5" stop-color="#8b5588"/>
<stop offset="1" stop-color="#34366e"/>
</linearGradient>
<radialGradient id="gLamp">
<stop offset="0" stop-color="#ffe9a8" stop-opacity=".85"/>
<stop offset=".4" stop-color="#ffc977" stop-opacity=".25"/>
<stop offset="1" stop-color="#ffb060" stop-opacity="0"/>
</radialGradient>
<linearGradient id="gFeather" x1="1" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fffaf0"/>
<stop offset=".55" stop-color="#f4eadd"/>
<stop offset="1" stop-color="#d6c3c2"/>
</linearGradient>
<linearGradient id="gBeak" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#ffd65e"/>
<stop offset="1" stop-color="#f59e35"/>
</linearGradient>
<linearGradient id="gPouch" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f7954a"/>
<stop offset="1" stop-color="#dd6238"/>
</linearGradient>
<linearGradient id="gDeck" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e4ad72"/>
<stop offset="1" stop-color="#b87a47"/>
</linearGradient>
</defs>

<!-- 天空 -->
<rect x="-1400" y="-1600" width="4000" height="1601" fill="#2b2152"/>
<rect x="-1400" y="0" width="4000" height="422" fill="url(#gSky)"/>
<g id="stars"></g>
<circle cx="840" cy="400" r="320" fill="url(#gGlow)"/>
<circle cx="840" cy="402" r="78" fill="url(#gSun)"/>
<g id="clouds"></g>
<g id="hillsFar" opacity=".9"></g>
<g id="hillsNear"></g>

<!-- 海 -->
<rect x="-1400" y="420" width="4000" height="281" fill="url(#gSea)"/>
<rect x="-1400" y="700" width="4000" height="1600" fill="#34366e"/>
<ellipse cx="840" cy="424" rx="300" ry="42" fill="url(#gGlow)" opacity=".75"/>
<rect x="-1400" y="419.4" width="4000" height="1.4" fill="#ffe6b0" opacity=".6"/>
<g id="reflect"></g>
<g id="boats"></g>
<g id="wavesFar"></g>
<g id="wavesNear"></g>
<g id="gulls"></g>

<!-- 栈桥 -->
<g id="lamps"></g>
<g id="rail"></g>
<g id="pilings"></g>
<rect x="-1400" y="662" width="4000" height="1640" fill="#2b2c60" opacity=".88"/>
<rect x="-1400" y="661.5" width="4000" height="1.5" fill="#ffcdbf" opacity=".35"/>
<g id="foam"></g>
<rect x="-1400" y="562" width="4000" height="10" fill="url(#gDeck)"/>
<rect x="-1400" y="572" width="4000" height="30" fill="#7a4c32"/>
<rect x="-1400" y="572" width="4000" height="2.5" fill="#5a3525"/>
<rect x="-1400" y="586" width="4000" height="1.2" fill="#5c3827" opacity=".8"/>
<rect x="-1400" y="599" width="4000" height="3" fill="#4b2c1f"/>
<g id="seams"></g>
<ellipse cx="545" cy="565" rx="190" ry="6" fill="#3b1c2a" opacity=".3"/>

<!-- 车轮 -->
<g>
<circle cx="430" cy="490" r="70" fill="none" stroke="#231c2c" stroke-width="9"/>
<circle cx="430" cy="490" r="65.5" fill="none" stroke="#efe2cc" stroke-width="1.6" opacity=".75"/>
<circle cx="430" cy="490" r="61" fill="none" stroke="#c7cfd6" stroke-width="3"/>
<circle cx="430" cy="490" r="60" fill="#e6ebef" opacity=".07"/>
<g id="spokesR"></g>
<circle cx="430" cy="490" r="7" fill="#a7b0b8" stroke="#5b636c" stroke-width="2"/>
</g>
<g>
<circle cx="650" cy="490" r="70" fill="none" stroke="#231c2c" stroke-width="9"/>
<circle cx="650" cy="490" r="65.5" fill="none" stroke="#efe2cc" stroke-width="1.6" opacity=".75"/>
<circle cx="650" cy="490" r="61" fill="none" stroke="#c7cfd6" stroke-width="3"/>
<circle cx="650" cy="490" r="60" fill="#e6ebef" opacity=".07"/>
<g id="spokesF"></g>
<circle cx="650" cy="490" r="7" fill="#a7b0b8" stroke="#5b636c" stroke-width="2"/>
</g>
<path d="M353.2 503.5 A78 78 0 0 1 456.7 416.7" fill="none" stroke="#1f9e97" stroke-width="6" stroke-linecap="round"/>
<path d="M590.3 439.9 A78 78 0 0 1 726.8 476.4" fill="none" stroke="#1f9e97" stroke-width="6" stroke-linecap="round"/>

<!-- 远侧腿 -->
<g stroke-linecap="round">
<line id="fShin" stroke="#bf672a" stroke-width="7"/>
<line id="fThighO" stroke="#b9aaa3" stroke-width="16"/>
<line id="fThigh" stroke="#d3c6bd" stroke-width="13"/>
<rect id="fPedal" width="22" height="6" rx="2" fill="#241f2b"/>
<g id="fFoot">
<path d="M-5 -2 C2 -5 12 -4 22 -2 C27 0 26 3 22 4 C26 6 25 9 21 9 C13 9 3 8 -5 6 C-8 4 -8 0 -5 -2 Z" fill="#b95f27"/>
</g>
<line id="fCrank" stroke="#7b848d" stroke-width="6"/>
</g>

<!-- 车架 -->
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M430 490 L530 500 L500 392 Z" stroke="#1f9e97" stroke-width="7"/>
<path d="M500 392 L622 392 M530 500 L632 425" stroke="#1f9e97" stroke-width="8"/>
<path d="M621 388 L633 428" stroke="#177f7a" stroke-width="12"/>
<path d="M632 426 C639 455 643 475 650 490" stroke="#1f9e97" stroke-width="6"/>
<path d="M506 389 L618 389 M536 492 L626 426" stroke="#86ddd5" stroke-width="2" opacity=".7"/>
<path d="M500 392 L497 377" stroke="#aeb7bf" stroke-width="5"/>
<path d="M621 390 L617 366" stroke="#aeb7bf" stroke-width="5"/>
<path d="M617 366 C611 355 600 353 588 359" stroke="#c9d0d6" stroke-width="5"/>
<path d="M592 357 L583 362" stroke="#6a3a2a" stroke-width="8"/>
<circle cx="430" cy="490" r="8" stroke="#8d969e" stroke-width="3"/>
<path id="chain" d="M530 483 L430 482 A8 8 0 0 0 430 498 L530 517 A17 17 0 0 0 530 483" stroke="#4c4452" stroke-width="2.5" stroke-dasharray="3 2.5"/>
</g>
<g id="ring">
<circle cx="530" cy="500" r="18" fill="none" stroke="#b9c1c8" stroke-width="4" stroke-dasharray="2.2 1.6"/>
<circle cx="530" cy="500" r="15" fill="none" stroke="#9aa3ab" stroke-width="2"/>
<path d="M530 500 L530 486 M530 500 L543.3 495.7 M530 500 L538.2 511.3 M530 500 L521.8 511.3 M530 500 L516.7 495.7" stroke="#9aa3ab" stroke-width="3" stroke-linecap="round"/>
<circle cx="530" cy="500" r="5" fill="#6f7880"/>
</g>
<path d="M476 373 C478 365 494 364 506 366 L522 369 C528 372 524 378 516 379 L486 380 C477 380 474 377 476 373 Z" fill="#6b3a26"/>

<!-- 车篮与鱼 -->
<g>
<path d="M628 398 L640 398" stroke="#aeb7bf" stroke-width="3" stroke-linecap="round"/>
<g id="fish1" transform="translate(652 360) rotate(-12)">
<path d="M0 -16 C7 -10 8 8 0 26 C-8 8 -7 -10 0 -16 Z" fill="#9cc3d6"/>
<path d="M0 -14 L-9 -29 L0 -23 L9 -29 Z" fill="#86afc6"/>
<path d="M0 -15 C3 -6 3 8 0 22" fill="none" stroke="#6f98b3" stroke-width="1.6"/>
</g>
<g id="fish2" transform="translate(678 359) rotate(15)">
<path d="M0 -16 C7 -10 8 8 0 26 C-8 8 -7 -10 0 -16 Z" fill="#b1d0de"/>
<path d="M0 -14 L-9 -29 L0 -23 L9 -29 Z" fill="#93b8cc"/>
<path d="M0 -15 C3 -6 3 8 0 22" fill="none" stroke="#7aa2bb" stroke-width="1.6"/>
</g>
<path d="M636 362 L694 362 L688 408 L642 408 Z" fill="#c98c4d" stroke="#8b5a2e" stroke-width="2" stroke-linejoin="round"/>
<path d="M637 373 L693 373 M638.5 385 L691.5 385 M640 397 L690 397" stroke="#9a6534" stroke-width="1.6"/>
<path d="M650 362 L652 408 M665 362 L665 408 M680 362 L678 408" stroke="#a86f3b" stroke-width="1.4"/>
<rect x="633" y="357" width="64" height="7" rx="3.5" fill="#a86d38"/>
</g>

<!-- 鹈鹕:身体(后层) -->
<g id="pBack">
<g id="wingF">
<path d="M505 276 C548 260 584 272 598 300 C606 318 606 338 604 356 C596 346 584 336 570 330 Z" fill="#d2c3bb"/>
<path d="M586 304 C598 320 604 338 604 356 C596 348 588 340 578 334 C584 326 588 314 586 304 Z" fill="#1f1b26"/>
</g>
<path d="M428 300 L384 286 L394 301 L380 314 L396 316 L386 328 L432 326 Z" fill="#e3d6ca"/>
<path d="M420 300 C430 255 520 245 565 265 C600 280 595 340 560 360 C520 382 460 372 435 345 C422 332 415 315 420 300 Z" fill="url(#gFeather)" stroke="#cdbab4" stroke-width="1.5"/>
<path d="M566 270 C594 286 592 334 562 356 C578 332 582 298 562 274 Z" fill="#ffd6a0" opacity=".7"/>
</g>

<!-- 近侧腿 -->
<g stroke-linecap="round">
<line id="nCrank" stroke="#b3bcc4" stroke-width="6"/>
<circle cx="530" cy="500" r="4.5" fill="#8a939b"/>
<rect id="nPedal" width="22" height="6" rx="2" fill="#3a3340"/>
<line id="nShin" stroke="#f08d38" stroke-width="7.5"/>
<g id="nFoot">
<path d="M-5 -2 C2 -5 12 -4 22 -2 C27 0 26 3 22 4 C26 6 25 9 21 9 C13 9 3 8 -5 6 C-8 4 -8 0 -5 -2 Z" fill="#f08632"/>
<path d="M1 2 L20 1 M1 5 L19 7" fill="none" stroke="#c9621f" stroke-width="1"/>
</g>
<line id="nThighO" stroke="#cdbdb3" stroke-width="17"/>
<line id="nThigh" stroke="#f3eadf" stroke-width="14"/>
</g>

<!-- 鹈鹕:翅膀、围巾、头(前层) -->
<g id="pFront">
<g id="wingN">
<path d="M462 292 C500 262 552 266 578 296 C590 314 595 340 596 364 C584 354 568 342 548 336 C515 328 480 318 462 292 Z" fill="#f5ede3" stroke="#d6c7bd" stroke-width="1.5"/>
<path d="M568 300 C586 318 594 342 596 364 C586 356 574 346 562 338 C570 326 572 312 568 300 Z" fill="#26212e"/>
<path d="M486 298 C514 296 540 304 560 318 M496 310 C522 311 544 320 556 330" fill="none" stroke="#dccfc4" stroke-width="2" stroke-linecap="round"/>
</g>
<path id="scarfB" fill="#b02a43"/>
<path id="scarfA" fill="#d83a4f"/>
<g id="headG">
<path d="M548 292 C538 256 576 242 571 212 C568 196 574 184 588 180" fill="none" stroke="#f6eee4" stroke-width="30" stroke-linecap="round"/>
<path d="M559 286 C553 256 587 244 582 214" fill="none" stroke="#ffd8a8" stroke-width="4.5" stroke-linecap="round" opacity=".55"/>
<path d="M534 252 Q551 262 569 250 L570 262 Q551 274 533 264 Z" fill="#d83a4f"/>
<circle cx="534" cy="259" r="6" fill="#b82e46"/>
<path d="M574 164 C562 152 552 160 546 150 C552 166 562 172 572 172 Z" fill="#efe3d4"/>
<ellipse cx="593" cy="176" rx="25" ry="21" fill="#f7efe5"/>
<ellipse cx="588" cy="163" rx="15" ry="7" fill="#f7dc98" opacity=".55"/>
<g id="pouch">
<path d="M611 192 C652 201 700 213 745 222 C724 246 684 264 650 256 C624 250 600 232 590 205 C596 198 604 194 611 192 Z" fill="url(#gPouch)"/>
<path d="M628 214 C650 226 682 234 712 232" fill="none" stroke="#ffbf85" stroke-width="3" stroke-linecap="round" opacity=".6"/>
</g>
<path d="M606 163 C650 170 702 187 744 204 C755 209 755 221 745 223 C700 213 652 201 611 192 Z" fill="url(#gBeak)"/>
<path d="M611 192 C652 201 700 213 745 223" fill="none" stroke="#d2702e" stroke-width="2"/>
<path d="M613 172 C660 180 704 194 738 207" fill="none" stroke="#e38c30" stroke-width="1.4" opacity=".8"/>
<path d="M742 204 C755 208 757 222 745 224 C749 216 748 209 742 204 Z" fill="#d6503b"/>
<circle cx="600" cy="170" r="8" fill="#f2c39c"/>
<g id="eye">
<circle cx="600" cy="170" r="4.3" fill="#2a1b1f"/>
<circle cx="601.6" cy="168.4" r="1.4" fill="#fff"/>
</g>
</g>
</g>
</svg>

<script>
(() => {
const NS = 'http://www.w3.org/2000/svg';
const $ = id => document.getElementById(id);
const el = (tag, attrs, parent) => {
const e = document.createElementNS(NS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(e);
return e;
};
const TAU = Math.PI * 2, DEG = 180 / Math.PI;
let seed = 20260922;
const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
const X0 = -1400, X1 = 2600;

// 运动参数:踏频、齿比 → 车轮角速度 → 地面速度
const CRANK_W = TAU / 1.25;
const WHEEL_W = CRANK_W * 1.45;
const V = WHEEL_W * 70;

// 星星
const stars = $('stars');
for (let i = 0; i < 150; i++) {
const x = X0 + rnd() * (X1 - X0);
const y = i < 90 ? 6 + Math.pow(rnd(), 1.7) * 230 : -1500 + rnd() * 1500;
const c = el('circle', {cx: x.toFixed(1), cy: y.toFixed(1), r: (0.5 + rnd() * 1.2).toFixed(2), fill: '#fff4e2', class: 'tw'}, stars);
c.style.animationDuration = (2 + rnd() * 3).toFixed(2) + 's';
c.style.animationDelay = (-rnd() * 5).toFixed(2) + 's';
}

// 可平铺滚动图层
const layers = [];
function layer(id, speed, tile, build) {
const g = el('g', {}, $(id));
const k0 = Math.floor(X0 / tile) - 1, k1 = Math.ceil(X1 / tile) + 1;
for (let k = k0; k <= k1; k++) build(el('g', {transform: `translate(${k * tile} 0)`}, g));
layers.push({g, speed, tile});
}

// 云
const cloudDefs = [];
for (let i = 0; i < 6; i++) cloudDefs.push({x: i * 265 + rnd() * 60, y: 70 + rnd() * 240, w: 130 + rnd() * 170});
function cloud(p, d) {
const high = d.y < 190;
const body = high ? '#b0679a' : '#f2968e';
const lit = high ? '#f0a4ae' : '#ffd29f';
const h = 13 + d.w * 0.03;
const g = el('g', {opacity: high ? 0.75 : 0.85}, p);
el('rect', {x: d.x, y: d.y, width: d.w, height: h, rx: h / 2, fill: body}, g);
el('ellipse', {cx: d.x + d.w * 0.3, cy: d.y + 2, rx: d.w * 0.16, ry: h * 0.95, fill: body}, g);
el('ellipse', {cx: d.x + d.w * 0.55, cy: d.y - 1, rx: d.w * 0.19, ry: h * 1.3, fill: body}, g);
el('ellipse', {cx: d.x + d.w * 0.77, cy: d.y + 3, rx: d.w * 0.11, ry: h * 0.8, fill: body}, g);
el('rect', {x: d.x + h * 0.5, y: d.y + h - 3.5, width: d.w - h, height: 3, rx: 1.5, fill: lit}, g);
}
layer('clouds', 14, 1600, s => cloudDefs.forEach(d => cloud(s, d)));

// 远山与岬角
function hillPath(T, f) {
const N = Math.round(T / 8);
let d = 'M-2 422';
for (let i = 0; i <= N; i++) {
const x = -2 + i * (T + 4) / N;
d += ` L${x.toFixed(1)} ${(420 - Math.max(0, f(x))).toFixed(1)}`;
}
return d + ` L${T + 2} 422 Z`;
}
const farF = x => 26 * Math.sin(TAU * 2 * x / 2400 + 1.3) + 10 * Math.sin(TAU * 5 * x / 2400 + 0.5) + 3;
const nearF = x => 42 * Math.sin(TAU * 3 * x / 3000 + 0.4) + 14 * Math.sin(TAU * 8 * x / 3000 + 2) - 6;
layer('hillsFar', 5, 2400, s => el('path', {d: hillPath(2400, farF), fill: '#c27b9c'}, s));

let xm = 0, hm = -1;
for (let x = 0; x < 3000; x += 3) { const h = nearF(x); if (h > hm) { hm = h; xm = x; } }
const beams = [];
layer('hillsNear', 11, 3000, s => {
el('path', {d: hillPath(3000, nearF), fill: '#6d4677'}, s);
const x = xm, b = 420 - hm + 5, ly = b - 45;
const beam = el('path', {d: `M${x} ${ly} L${x - 240} ${ly - 20} L${x - 240} ${ly + 20} Z`, fill: '#fff2c4', opacity: 0.25}, s);
el('path', {d: `M${x + 11} ${b} L${x + 11} ${b - 9} L${x + 18} ${b - 15} L${x + 25} ${b - 9} L${x + 25} ${b} Z`, fill: '#553660'}, s);
el('path', {d: `M${x - 8} ${b} L${x + 8} ${b} L${x + 5} ${b - 40} L${x - 5} ${b - 40} Z`, fill: '#efe2e8'}, s);
el('path', {d: `M${x - 7.3} ${b - 9} L${x + 7.3} ${b - 9} L${x + 6.5} ${b - 17} L${x - 6.5} ${b - 17} Z M${x - 5.9} ${b - 25} L${x + 5.9} ${b - 25} L${x + 5.3} ${b - 32} L${x - 5.3} ${b - 32} Z`, fill: '#c9465d'}, s);
el('rect', {x: x - 8, y: b - 42, width: 16, height: 3, fill: '#3b2847'}, s);
el('rect', {x: x - 5, y: b - 50, width: 10, height: 9, fill: '#ffe9a6'}, s);
el('path', {d: `M${x - 7.5} ${b - 50} L${x} ${b - 58} L${x + 7.5} ${b - 50} Z`, fill: '#3b2847'}, s);
const flash = el('circle', {cx: x, cy: ly, r: 18, fill: 'url(#gLamp)', opacity: 0}, s);
beams.push({beam, flash, x, y: ly});
});

// 海面倒影
const refl = $('reflect'), refLines = [];
for (let i = 0; i < 16; i++) {
const y = 425 + i * 6 + i * i * 0.18;
const w = (36 + i * 7) * (0.55 + rnd() * 0.6);
const x = 840 + (rnd() - 0.5) * (20 + i * 3);
const e = el('line', {x1: (x - w / 2).toFixed(1), y1: y.toFixed(1), x2: (x + w / 2).toFixed(1), y2: y.toFixed(1), stroke: '#ffe3a6', 'stroke-width': (2 + i * 0.12).toFixed(2), 'stroke-linecap': 'round'}, refl);
refLines.push({e, ph: rnd() * TAU, sp: 2 + rnd() * 3});
}

// 帆船
function boat(p, x, y, k) {
const g = el('g', {transform: `translate(${x} ${y}) scale(${k})`, fill: '#4b3161'}, p);
el('path', {d: 'M-22 -5 L22 -5 L16 2 L-16 2 Z'}, g);
el('path', {d: 'M-1 -7 L-1 -48 L-21 -9 Z'}, g);
el('path', {d: 'M2 -9 L2 -42 L18 -9 Z'}, g);
}
layer('boats', 7, 1900, s => { boat(s, 320, 420, 0.9); boat(s, 1260, 421, 0.6); });

// 波纹
function waveLayer(id, speed, tile, n, y0, y1, col) {
const defs = [];
for (let i = 0; i < n; i++) {
const y = y0 + rnd() * (y1 - y0);
defs.push({x: rnd() * (tile - 40), y, w: 5 + (y - 420) * 0.12});
}
layer(id, speed, tile, s => defs.forEach(d => el('path', {
d: `M${d.x.toFixed(1)} ${d.y.toFixed(1)} q${(d.w / 2).toFixed(1)} ${(-d.w * 0.35).toFixed(1)} ${d.w.toFixed(1)} 0 q${(d.w / 2).toFixed(1)} ${(-d.w * 0.35).toFixed(1)} ${d.w.toFixed(1)} 0`,
fill: 'none', stroke: col, 'stroke-width': 1.3, 'stroke-linecap': 'round', opacity: (0.18 + (d.y - 420) / 500).toFixed(2)
}, s)));
}
waveLayer('wavesFar', 16, 480, 12, 428, 478, '#ffe0cc');
waveLayer('wavesNear', 55, 420, 12, 478, 552, '#ffd3c6');

// 海鸥
const gulls = [];
[[160, 150, 1, 34], [520, 105, 0.72, 24], [980, 205, 0.9, 42], [760, 62, 0.58, 18], [1320, 170, 0.8, 30]].forEach(([x, y, k, sp], i) => {
const g = el('g', {}, $('gulls'));
const p = el('path', {fill: 'none', stroke: '#3a2650', 'stroke-width': 2.6, 'stroke-linecap': 'round', 'stroke-linejoin': 'round'}, g);
gulls.push({g, p, x, y, k, sp, ph: i * 1.9, fl: 6.5 + i * 0.7});
});

// 栈桥:路灯、绳栏、桩、浪花、木板缝(与地面同速)
layer('lamps', V, 760, s => {
const x = 380;
el('circle', {cx: x, cy: 330, r: 62, fill: 'url(#gLamp)'}, s);
el('rect', {x: x - 3, y: 342, width: 6, height: 210, fill: '#30243c'}, s);
el('rect', {x: x - 9, y: 538, width: 18, height: 24, rx: 3, fill: '#30243c'}, s);
el('rect', {x: x - 7, y: 360, width: 14, height: 4, rx: 2, fill: '#30243c'}, s);
el('path', {d: `M${x - 9} 318 L${x + 9} 318 L${x + 6} 342 L${x - 6} 342 Z`, fill: '#ffe7a2'}, s);
el('path', {d: `M${x} 318 L${x} 342`, stroke: '#30243c', 'stroke-width': 1.6}, s);
el('path', {d: `M${x - 13} 319 L${x} 304 L${x + 13} 319 Z`, fill: '#30243c'}, s);
el('circle', {cx: x, cy: 301, r: 3, fill: '#30243c'}, s);
el('rect', {x: x - 8, y: 341, width: 16, height: 4, rx: 2, fill: '#30243c'}, s);
});
layer('rail', V, 175, s => {
el('path', {d: 'M0 525 Q87.5 546 175 525', fill: 'none', stroke: '#e3c392', 'stroke-width': 2.6}, s);
el('rect', {x: -5, y: 516, width: 10, height: 48, rx: 2, fill: '#6a422d'}, s);
el('rect', {x: -6.5, y: 512, width: 13, height: 6, rx: 3, fill: '#8d5c3c'}, s);
});
layer('pilings', V, 175, s => {
el('path', {d: 'M12 610 L163 656 M12 656 L163 610', stroke: '#48291d', 'stroke-width': 6}, s);
el('rect', {x: -12, y: 598, width: 24, height: 110, fill: '#5a3727'}, s);
el('rect', {x: -12, y: 598, width: 7, height: 110, fill: '#6f4633'}, s);
});
layer('foam', V, 175, s => {
el('ellipse', {cx: 0, cy: 663, rx: 20, ry: 3, fill: '#ffd6cb', opacity: 0.5}, s);
el('path', {d: 'M48 676 q10 -3 20 0 M112 688 q12 -3 24 0', fill: 'none', stroke: '#ffd6cb', 'stroke-width': 1.3, opacity: 0.3, 'stroke-linecap': 'round'}, s);
});
layer('seams', V, 44, s => {
el('rect', {x: -0.8, y: 574, width: 1.6, height: 25, fill: '#5a3625'}, s);
el('rect', {x: 21.4, y: 563, width: 1.2, height: 8, fill: '#a96f40'}, s);
el('circle', {cx: 5, cy: 580, r: 1.1, fill: '#4a2b1e'}, s);
el('circle', {cx: 5, cy: 593, r: 1.1, fill: '#4a2b1e'}, s);
});

// 辐条
function spokes(id, cx, cy) {
const g = $(id);
for (let i = 0; i < 18; i++) {
const a = i / 18 * TAU;
el('line', {x1: cx, y1: cy, x2: (cx + 60 * Math.cos(a)).toFixed(2), y2: (cy + 60 * Math.sin(a)).toFixed(2), stroke: '#dde3e8', 'stroke-width': 1.1}, g);
}
el('rect', {x: cx + 34, y: cy - 2.5, width: 10, height: 5, rx: 1.5, fill: '#ff8a3d'}, g);
return g;
}
const spR = spokes('spokesR', 430, 490), spF = spokes('spokesF', 650, 490);

// 腿:两段式反向运动学,脚始终踩在踏板上
const BB = {x: 530, y: 500}, CR = 28, LA = 84, LB = 94;
const mk = (p, hx, hy, off) => ({
crank: $(p + 'Crank'), pedal: $(p + 'Pedal'), shin: $(p + 'Shin'), foot: $(p + 'Foot'),
thighs: [$(p + 'ThighO'), $(p + 'Thigh')], hx, hy, off
});
const legN = mk('n', 522, 348, 0), legF = mk('f', 512, 348, Math.PI);
const setLine = (e, x1, y1, x2, y2) => {
e.setAttribute('x1', x1.toFixed(2)); e.setAttribute('y1', y1.toFixed(2));
e.setAttribute('x2', x2.toFixed(2)); e.setAttribute('y2', y2.toFixed(2));
};
function leg(L, th, bob) {
const a = th + L.off;
const px = BB.x + CR * Math.cos(a), py = BB.y + CR * Math.sin(a);
setLine(L.crank, BB.x, BB.y, px, py);
L.pedal.setAttribute('x', (px - 11).toFixed(2));
L.pedal.setAttribute('y', (py - 3).toFixed(2));
const tx = px - 5, ty = py - 9, hx = L.hx, hy = L.hy + bob;
let d = Math.hypot(tx - hx, ty - hy);
d = Math.min(Math.max(d, Math.abs(LA - LB) + 1), (LA + LB) * 0.999);
const phi = Math.atan2(ty - hy, tx - hx);
const al = Math.acos((LA * LA + d * d - LB * LB) / (2 * LA * d));
const kx = hx + LA * Math.cos(phi - al), ky = hy + LA * Math.sin(phi - al);
for (const e of L.thighs) setLine(e, hx, hy, kx, ky);
setLine(L.shin, kx, ky, tx, ty);
L.foot.setAttribute('transform', `translate(${tx.toFixed(2)} ${ty.toFixed(2)}) rotate(${(7 * Math.sin(a)).toFixed(2)})`);
}

// 围巾飘带
function ribbon(x0, y0, len, w0, droop, amp, freq, ph, t) {
const N = 14, top = [], bot = [];
for (let i = 0; i <= N; i++) {
const s = i / N;
const x = x0 - len * s + 4 * s * Math.sin(t * 9 + ph + s * 3);
const y = y0 + droop * s * s + amp * s * Math.sin(s * freq - t * 13 + ph);
const w = w0 * (1 - 0.55 * s);
top.push(`${x.toFixed(1)} ${(y - w / 2).toFixed(1)}`);
bot.push(`${x.toFixed(1)} ${(y + w / 2).toFixed(1)}`);
}
return 'M' + top.join(' L') + ' L' + bot.reverse().join(' L') + ' Z';
}

const pBack = $('pBack'), pFront = $('pFront'), wingN = $('wingN'), wingF = $('wingF');
const headG = $('headG'), pouch = $('pouch'), eye = $('eye');
const scarfA = $('scarfA'), scarfB = $('scarfB');
const fish1 = $('fish1'), fish2 = $('fish2');
const ring = $('ring'), chain = $('chain');
let nextBlink = 2;

function frame(now) {
const t = now / 1000;
const th = t * CRANK_W;

// 身体随踩踏轻微起伏,翅膀反向微转保持握把
const bob = 1.6 * Math.sin(2 * th + 0.8);
const pt = `translate(0 ${bob.toFixed(2)})`;
pBack.setAttribute('transform', pt);
pFront.setAttribute('transform', pt);
const wr = (-bob / 96 * DEG).toFixed(2);
wingN.setAttribute('transform', `rotate(${wr} 500 285)`);
wingF.setAttribute('transform', `rotate(${wr} 505 280)`);

leg(legF, th, bob);
leg(legN, th, bob);

ring.setAttribute('transform', `rotate(${(th * DEG % 360).toFixed(2)} 530 500)`);
chain.setAttribute('stroke-dashoffset', ((th * 18) % 5.5).toFixed(2));
const wa = (t * WHEEL_W * DEG % 360).toFixed(2);
spR.setAttribute('transform', `rotate(${wa} 430 490)`);
spF.setAttribute('transform', `rotate(${wa} 650 490)`);

headG.setAttribute('transform', `rotate(${(1.6 * Math.sin(2 * th + 2)).toFixed(2)} 550 288)`);
const ps = 1 + 0.07 * Math.sin(2 * th + 2.6);
pouch.setAttribute('transform', `translate(0 ${(195 * (1 - ps)).toFixed(2)}) scale(1 ${ps.toFixed(3)})`);

let es = 1;
const bt = t - nextBlink;
if (bt > 0) {
if (bt < 0.18) es = 1 - 0.92 * Math.sin(Math.PI * bt / 0.18);
else nextBlink = t + 2.2 + Math.random() * 3.5;
}
eye.setAttribute('transform', `translate(0 ${(170 * (1 - es)).toFixed(2)}) scale(1 ${es.toFixed(3)})`);

scarfB.setAttribute('d', ribbon(536, 262, 62, 9, 18, 6, 6, 1.8, t));
scarfA.setAttribute('d', ribbon(536, 257, 80, 11, 10, 7, 7, 0, t));

fish1.setAttribute('transform', `translate(652 360) rotate(${(-12 + 8 * Math.sin(t * 7)).toFixed(2)})`);
fish2.setAttribute('transform', `translate(678 359) rotate(${(15 + 8 * Math.sin(t * 7 + 2.1)).toFixed(2)})`);

for (const L of layers) {
L.g.setAttribute('transform', `translate(${(-((t * L.speed) % L.tile)).toFixed(2)} 0)`);
}

const la = t * 1.3, c = Math.cos(la);
for (const B of beams) {
B.beam.setAttribute('transform', `translate(${B.x} ${B.y}) scale(${c.toFixed(3)} 1) translate(${-B.x} ${-B.y})`);
B.beam.setAttribute('opacity', (0.1 + 0.22 * Math.abs(c)).toFixed(3));
B.flash.setAttribute('opacity', (Math.sin(la) > 0 ? Math.pow(1 - Math.abs(c), 3) : 0).toFixed(3));
}

for (const R of refLines) R.e.setAttribute('opacity', (0.35 + 0.35 * Math.sin(t * R.sp + R.ph)).toFixed(3));

for (const G of gulls) {
const span = 2000;
const x = ((G.x - t * G.sp + 400) % span + span) % span - 400;
const y = G.y + 7 * Math.sin(t * 0.7 + G.ph);
const f = Math.sin(t * G.fl + G.ph);
const ty = -2 + 7 * f, cy = -7 + 3 * f;
G.p.setAttribute('d', `M-17 ${ty.toFixed(2)} Q-8 ${cy.toFixed(2)} 0 2 Q8 ${cy.toFixed(2)} 17 ${ty.toFixed(2)}`);
G.g.setAttribute('transform', `translate(${x.toFixed(1)} ${y.toFixed(1)}) scale(${G.k})`);
}

requestAnimationFrame(frame);
}
frame(performance.now());
})();
</script>
</body>
</html>

分别是网页版和code模式,效果差异挺大,但共同点是细节满满,截图看不出来的细节。




车铃铛能点击这件事,真的想不到它会做,我的提示词很简单。




<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>鹈鹕骑车记</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:#a7dcf5;font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif;color:#243044;-webkit-user-select:none;user-select:none}
#scene{position:fixed;inset:0;width:100%;height:100%;display:block;cursor:pointer}
.hud{position:fixed;left:28px;top:22px;pointer-events:none}
.hud h1{font-size:clamp(28px,4.2vw,46px);font-weight:900;letter-spacing:3px;color:#fff;text-shadow:0 3px 0 #2b6f9e,0 8px 20px rgba(0,40,80,.25)}
.hud p{margin-top:6px;font-size:14px;color:#1d4b6e;opacity:.85}
.odo{position:fixed;right:28px;top:26px;background:rgba(255,255,255,.78);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);padding:8px 16px;border-radius:14px;font-weight:700;font-size:14px;box-shadow:0 4px 14px rgba(0,50,90,.15)}
.odo b{font-variant-numeric:tabular-nums;font-size:22px;color:#e8483f;margin:0 2px}
.ctrl{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);display:flex;align-items:center;gap:12px;background:rgba(255,255,255,.82);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);padding:10px 18px;border-radius:999px;box-shadow:0 6px 20px rgba(0,40,70,.18);white-space:nowrap}
.ctrl input[type=range]{width:min(38vw,220px);accent-color:#e8483f;cursor:pointer}
.ctrl .lbl{font-size:13px;font-weight:700;color:#40546b}
.ctrl .val{font-size:13px;font-weight:800;color:#e8483f;min-width:40px;font-variant-numeric:tabular-nums}
.ctrl button{border:0;background:#ffcf3f;color:#5a3b00;font-weight:800;padding:8px 16px;border-radius:999px;cursor:pointer;box-shadow:0 3px 0 #d69b00;font-size:14px;font-family:inherit}
.ctrl button:active{transform:translateY(2px);box-shadow:0 1px 0 #d69b00}
.spark{animation:twinkle 2.6s ease-in-out infinite;opacity:0}
@keyframes twinkle{0%,100%{opacity:0}50%{opacity:.95}}
.beam{animation:beam 4s linear infinite;opacity:0}
@keyframes beam{0%,100%{opacity:0}12%,30%{opacity:.5}46%{opacity:0}}
.bob{animation:bob 3.4s ease-in-out infinite;transform-box:fill-box;transform-origin:50% 100%}
@keyframes bob{50%{transform:translateY(3px) rotate(2deg)}}
.ol{fill:#263041;stroke:#263041;stroke-width:7;stroke-linejoin:round}
.fl{fill:url(#gBody);stroke:none}
.ink{stroke:#263041;stroke-width:3;stroke-linejoin:round;stroke-linecap:round}
</style>
</head>
<body>
<div class="hud">
<h1>鹈鹕骑车记</h1>
<p>一只鹈鹕的海边兜风 · 点击画面按铃铛</p>
</div>
<div class="odo">已骑行 <b id="odo">0</b> 米</div>
<div class="ctrl">
<span class="lbl">🐢</span>
<input id="spd" type="range" min="0" max="250" value="100" aria-label="骑行速度">
<span class="lbl">🚀</span>
<span class="val" id="spdVal">1.0×</span>
<button id="bellBtn" type="button">🔔 按铃</button>
</div>

<svg id="scene" viewBox="0 0 1200 700" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gSky" gradientUnits="userSpaceOnUse" x1="0" y1="-500" x2="0" y2="400">
<stop offset="0" stop-color="#3f9ee0"/><stop offset=".6" stop-color="#9dd7f3"/><stop offset="1" stop-color="#ffe8c6"/>
</linearGradient>
<radialGradient id="gGlow"><stop offset="0" stop-color="#fff7cf" stop-opacity=".95"/><stop offset=".45" stop-color="#fff2b8" stop-opacity=".35"/><stop offset="1" stop-color="#fff2b8" stop-opacity="0"/></radialGradient>
<radialGradient id="gSun" cx=".4" cy=".35"><stop offset="0" stop-color="#fffbe0"/><stop offset="1" stop-color="#ffd35c"/></radialGradient>
<linearGradient id="gSea" gradientUnits="userSpaceOnUse" x1="0" y1="395" x2="0" y2="482">
<stop offset="0" stop-color="#93d3e8"/><stop offset="1" stop-color="#3b90c2"/>
</linearGradient>
<linearGradient id="gSand" gradientUnits="userSpaceOnUse" x1="0" y1="478" x2="0" y2="552">
<stop offset="0" stop-color="#f8e4b6"/><stop offset="1" stop-color="#ecc98a"/>
</linearGradient>
<linearGradient id="gBoard" gradientUnits="userSpaceOnUse" x1="0" y1="550" x2="0" y2="636">
<stop offset="0" stop-color="#d9a26b"/><stop offset="1" stop-color="#b87a45"/>
</linearGradient>
<linearGradient id="gFg" gradientUnits="userSpaceOnUse" x1="0" y1="650" x2="0" y2="760">
<stop offset="0" stop-color="#f1d49b"/><stop offset="1" stop-color="#e2b872"/>
</linearGradient>
<radialGradient id="gBody" gradientUnits="userSpaceOnUse" cx="600" cy="225" r="190">
<stop offset="0" stop-color="#ffffff"/><stop offset=".55" stop-color="#f6f9fc"/><stop offset="1" stop-color="#d5dfea"/>
</radialGradient>
<linearGradient id="gBeak" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd45e"/><stop offset="1" stop-color="#f3a42f"/></linearGradient>
<linearGradient id="gPouch" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffbe72"/><stop offset="1" stop-color="#ee8636"/></linearGradient>
<linearGradient id="gHelmet" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5aa4ff"/><stop offset="1" stop-color="#2463c9"/></linearGradient>

<!-- 鹈鹕白色部分:描边与填充共用同一形状,拼接处无缝 -->
<g id="dBody">
<path d="M504,306 L452,294 Q444,304 456,311 L448,318 Q448,330 462,329 L506,330 Z"/>
<path d="M488,316 C520,280 560,248 610,246 C650,244 674,272 662,305 C652,338 614,358 570,358 C535,358 505,340 488,316 Z"/>
</g>
<g id="dNeck">
<path d="M598,272 C594,236 630,222 648,182 L684,182 C686,214 660,232 664,284 Z"/>
<ellipse cx="668" cy="170" rx="27" ry="23"/>
</g>
</defs>

<!-- ===== 远景 ===== -->
<rect x="-1200" y="-800" width="3600" height="1210" fill="url(#gSky)"/>
<g id="sun">
<circle cx="985" cy="135" r="150" fill="url(#gGlow)"/>
<circle cx="985" cy="135" r="50" fill="url(#gSun)"/>
</g>
<g id="lyClouds"></g>
<g id="lyGulls"></g>
<rect x="-1200" y="395" width="3600" height="90" fill="url(#gSea)"/>
<rect x="-1200" y="394" width="3600" height="2.5" fill="#eef9ff" opacity=".85"/>
<g id="lyIsland"></g>
<g id="lyBoat"></g>
<g id="lySpark"></g>
<rect x="-1200" y="478" width="3600" height="80" fill="url(#gSand)"/>
<g id="lyFoam"></g>
<g id="lyBeach"></g>

<!-- ===== 栈道 ===== -->
<g id="lyRail"></g>
<rect x="-1200" y="505" width="3600" height="5" rx="2" fill="#7a5234"/>
<rect x="-1200" y="526" width="3600" height="4" rx="2" fill="#7a5234"/>
<rect x="-1200" y="550" width="3600" height="88" fill="url(#gBoard)"/>
<rect x="-1200" y="550" width="3600" height="5" fill="#e8b884"/>
<g id="lyPlanks"></g>
<rect x="-1200" y="636" width="3600" height="16" fill="#8e5b34"/>
<rect x="-1200" y="652" width="3600" height="700" fill="url(#gFg)"/>
<g id="lyPiles"></g>
<g id="lyFg"></g>

<!-- ===== 自行车 + 鹈鹕 ===== -->
<g id="rig">
<ellipse id="shadow" cx="600" cy="585" rx="220" ry="9" fill="#5b3a1e" opacity=".22"/>
<!-- 远侧:曲柄、脚蹬、腿 -->
<line id="farCrank" x1="585" y1="510" x2="585" y2="542" stroke="#6f7680" stroke-width="7" stroke-linecap="round"/>
<rect id="farPedal" x="573" y="539" width="24" height="7" rx="2" fill="#3a3e45"/>
<path id="farLegO" fill="none" stroke="#263041" stroke-width="15" stroke-linecap="round" stroke-linejoin="round"/>
<path id="farLegF" fill="none" stroke="#c9691c" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<g id="farFoot"><path d="M-3,-5 C6,-4 16,-1 25,2 Q30,5 25,8 Q20,8 21,11 Q24,14 19,16 Q15,15 14,16 Q14,20 9,19 L-2,9 Z" fill="#c9691c" class="ink"/></g>
<!-- 车轮(辐条由脚本生成) -->
<g id="wheelR"></g>
<g id="wheelF"></g>
<!-- 挡泥板 -->
<path d="M365.3,492.2 A90,90 0 0 1 523.9,442.1" fill="none" stroke="#263041" stroke-width="11" stroke-linecap="round"/>
<path d="M365.3,492.2 A90,90 0 0 1 523.9,442.1" fill="none" stroke="#f4ecd8" stroke-width="6" stroke-linecap="round"/>
<path d="M658.1,476.7 A90,90 0 0 1 831.9,476.7" fill="none" stroke="#263041" stroke-width="11" stroke-linecap="round"/>
<path d="M658.1,476.7 A90,90 0 0 1 831.9,476.7" fill="none" stroke="#f4ecd8" stroke-width="6" stroke-linecap="round"/>
<!-- 车架 -->
<path d="M730,425 Q744,468 745,500" fill="none" stroke="#263041" stroke-width="13" stroke-linecap="round"/>
<path d="M730,425 Q744,468 745,500" fill="none" stroke="#e8483f" stroke-width="8" stroke-linecap="round"/>
<path d="M455,500 L585,510 L555,395 Z M555,395 L720,390 M585,510 L730,425 M720,390 L730,425" fill="none" stroke="#263041" stroke-width="14" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M455,500 L585,510 L555,395 Z M555,395 L720,390 M585,510 L730,425 M720,390 L730,425" fill="none" stroke="#e8483f" stroke-width="9" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M562,392 L712,388 M592,504 L722,429" stroke="#ff8f86" stroke-width="2" stroke-linecap="round" opacity=".8"/>
<circle cx="745" cy="500" r="5" fill="#9aa3ad"/>
<!-- 链条与牙盘 -->
<path id="chain" d="M455,489 L585,488 A22,22 0 0 1 585,532 L455,511 A11,11 0 0 1 455,489 Z" fill="none" stroke="#3a3b42" stroke-width="3.2" stroke-dasharray="3 2"/>
<circle cx="455" cy="500" r="10" fill="#c3cad2" class="ink" style="stroke-width:2.5"/>
<g id="ring">
<circle cx="585" cy="510" r="24" fill="none" stroke="#6d7580" stroke-width="3" stroke-dasharray="2.5 2.2"/>
<circle cx="585" cy="510" r="21" fill="#d3d9e0" class="ink"/>
<path d="M585,510 L585,493 M585,510 L601.2,504.7 M585,510 L595,523.8 M585,510 L575,523.8 M585,510 L568.8,504.7" stroke="#8c95a1" stroke-width="4" stroke-linecap="round"/>
</g>
<!-- 座杆、坐垫 -->
<path d="M555,395 L548,364" stroke="#263041" stroke-width="10" stroke-linecap="round"/>
<path d="M555,395 L548,364" stroke="#b4bcc5" stroke-width="5.5" stroke-linecap="round"/>
<path d="M516,361 Q546,350 578,357 Q581,364 571,367 Q546,371 520,368 Q510,366 516,361 Z" fill="#6b3e26" class="ink"/>
<path d="M526,358 Q546,354 566,357" fill="none" stroke="#9a6444" stroke-width="2" stroke-linecap="round"/>
<!-- 车把 -->
<path d="M720,390 L716,364 C712,352 694,351 672,356" fill="none" stroke="#263041" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M720,390 L716,364 C712,352 694,351 672,356" fill="none" stroke="#c3cad3" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M677,356 L658,360" stroke="#263041" stroke-width="14" stroke-linecap="round"/>
<path d="M677,356 L658,360" stroke="#5a3a28" stroke-width="9" stroke-linecap="round"/>
<g id="bell">
<path d="M692,352 A9.5,9.5 0 0 1 711,352 Z" fill="#e6ebf0" class="ink" style="stroke-width:2.2"/>
<circle cx="701.5" cy="340" r="2.4" fill="#e6ebf0" class="ink" style="stroke-width:2"/>
<path d="M697,347 Q700,344 704,345" fill="none" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/>
</g>
<!-- 车筐 + 鱼 -->
<g id="basket">
<path d="M728,380 L718,377 M738,408 L732,428" stroke="#263041" stroke-width="6" stroke-linecap="round"/>
<path d="M728,380 L718,377 M738,408 L732,428" stroke="#b4bcc5" stroke-width="3" stroke-linecap="round"/>
<g id="fishTail"><path d="M748,372 C744,352 750,338 758,331 L750,321 Q760,322 768,325 L763,334 C767,348 764,362 762,372 Z" fill="#7fa7c4" class="ink"/></g>
<path d="M770,372 C768,352 776,338 790,333 C799,338 800,354 793,372 Z" fill="#9cbad1" class="ink"/>
<circle cx="789" cy="342" r="2.4" fill="#263041"/>
<path d="M782,351 Q788,355 795,351" fill="none" stroke="#6b8ca6" stroke-width="2" stroke-linecap="round"/>
<path d="M728,364 L798,364 L790,408 L736,408 Z" fill="#cf9552" class="ink"/>
<path d="M731,379 L795,379 M733,394 L793,394 M747,366 L749,407 M763,366 L763,407 M779,366 L777,407" stroke="#9a6432" stroke-width="2" stroke-linecap="round"/>
<rect x="724" y="360" width="78" height="8" rx="4" fill="#b77a3c" class="ink" style="stroke-width:2.5"/>
</g>
<!-- 近侧:曲柄、脚蹬、腿 -->
<line id="nearCrankO" x1="585" y1="510" x2="585" y2="478" stroke="#263041" stroke-width="11" stroke-linecap="round"/>
<line id="nearCrank" x1="585" y1="510" x2="585" y2="478" stroke="#c3cad3" stroke-width="6.5" stroke-linecap="round"/>
<circle cx="585" cy="510" r="5" fill="#8c95a1" class="ink" style="stroke-width:2"/>
<rect id="nearPedal" x="573" y="475" width="24" height="7" rx="2" fill="#3e434b" class="ink" style="stroke-width:2"/>
<path id="nearLegO" fill="none" stroke="#263041" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path id="nearLegF" fill="none" stroke="#f28c28" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>
<g id="nearFoot"><path d="M-3,-5 C6,-4 16,-1 25,2 Q30,5 25,8 Q20,8 21,11 Q24,14 19,16 Q15,15 14,16 Q14,20 9,19 L-2,9 Z" fill="#f28c28" class="ink"/></g>
<g id="pel">
<!-- 围巾飘带(路径由脚本实时计算) -->
<path id="scarfB" fill="#0f8a7f" class="ink" style="stroke-width:2.5"/>
<path id="scarfA" fill="#19b3a3" class="ink" style="stroke-width:2.5"/>
<!-- 白色身体:先统一描边,再统一填充 -->
<g class="ol"><use href="#dBody"/><use href="#dNeck"/></g>
<g class="fl"><use href="#dBody"/><use href="#dNeck"/></g>
<path d="M470,306 L500,313 M466,320 L500,321" stroke="#c9d4df" stroke-width="2" stroke-linecap="round"/>
<!-- 围巾 -->
<path d="M600,244 C620,254 648,254 670,240 L668,260 C646,272 620,270 601,262 Z" fill="#19b3a3" class="ink" style="stroke-width:2.5"/>
<path d="M616,250 L614,266 M636,253 L635,269 M655,250 L655,265" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity=".85"/>
<circle cx="603" cy="254" r="7" fill="#19b3a3" class="ink" style="stroke-width:2.5"/>
<!-- 头部零件:嘴、喉囊、眼、头盔 -->
<g id="headParts">
<path d="M690,186 L826,202 L812,213 L700,201 Z" fill="#a8423c"/>
<g id="pouch">
<path d="M688,186 C735,188 790,195 826,202 C812,214 790,228 760,236 C725,244 690,236 668,210 Z" fill="url(#gPouch)" class="ink"/>
<path d="M702,215 C727,227 754,227 782,216" fill="none" stroke="#d9722a" stroke-width="2" stroke-linecap="round" opacity=".7"/>
</g>
<path d="M684,158 C730,160 790,172 836,190 Q847,196 840,204 Q834,207 828,200 C790,192 735,187 688,187 Z" fill="url(#gBeak)" class="ink"/>
<path d="M693,167 C740,171 792,181 829,194" fill="none" stroke="#e39a22" stroke-width="2" stroke-linecap="round"/>
<path d="M834,191 Q846,196 840,203 Q835,206 830,200 Z" fill="#e8643c"/>
<g id="eye">
<ellipse cx="675" cy="168" rx="10" ry="8.5" fill="#ffd9a0"/>
<circle cx="675" cy="168" r="5.8" fill="#fff4bf" class="ink" style="stroke-width:2"/>
<circle cx="676.5" cy="168" r="3" fill="#1d1d1f"/>
<circle cx="677.6" cy="166.8" r="1.1" fill="#fff"/>
</g>
<path d="M646,166 Q656,184 668,196" fill="none" stroke="#263041" stroke-width="2"/>
<path d="M638,168 C633,140 660,127 685,133 C699,137 705,149 701,160 L688,157 C670,154 652,158 638,168 Z" fill="url(#gHelmet)" class="ink"/>
<path d="M652,145 C662,138 676,136 688,140 M646,156 C658,148 676,146 694,151" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity=".85"/>
</g>
<!-- 近侧翅膀:握住车把 -->
<g id="wing">
<path d="M652,346 Q646,357 634,350 Q626,361 614,350 Q604,359 594,346 Q582,353 574,338 Q561,341 556,322 L560,300 L650,320 Z" fill="#2e2f38" class="ink"/>
<path d="M560,275 C590,258 630,256 650,280 C662,296 668,318 670,340 L652,346 C640,330 622,320 600,322 C585,324 570,316 556,306 C548,298 548,285 560,275 Z" fill="#fbfcfe" class="ink"/>
<path d="M580,290 Q606,280 630,292 M592,306 Q616,298 640,312 M606,318 Q626,314 648,328" fill="none" stroke="#c8d3de" stroke-width="2" stroke-linecap="round"/>
<path d="M648,338 C662,333 681,341 685,354 C687,364 677,369 668,365 C661,362 655,357 649,352 Z" fill="#2e2f38" class="ink"/>
<path d="M662,342 Q672,349 674,360 M654,346 Q662,352 663,360" fill="none" stroke="#5b5d6b" stroke-width="1.6" stroke-linecap="round"/>
</g>
</g>
</g>
<g id="fx"></g>
<g id="bubble" opacity="0">
<path d="M14,0 H120 A14,14 0 0 1 134,14 V42 A14,14 0 0 1 120,56 H42 L26,72 L28,56 H14 A14,14 0 0 1 0,42 V14 A14,14 0 0 1 14,0 Z" fill="#fff" class="ink"/>
<text x="67" y="36" text-anchor="middle" font-size="22" font-weight="800" fill="#e8483f" font-family="PingFang SC,Microsoft YaHei,sans-serif">叮铃铃~</text>
</g>
</svg>
<script>
(() => {
const NS = 'http://www.w3.org/2000/svg';
const $ = id => document.getElementById(id);
function el(tag, attrs, parent) {
const n = document.createElementNS(NS, tag);
for (const k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
// 固定种子的随机数:每个平铺块内容完全一致,循环滚动无缝
function rng(seed) {
return () => (seed = (seed * 16807) % 2147483647) / 2147483647;
}
const INK = '#263041';

// ---------- 几何常量 ----------
const WHEEL_R = 80;
const RH = { x: 455, y: 500 }, FH = { x: 745, y: 500 }, BB = { x: 585, y: 510 };
const CRANK = 32;

// ---------- 车轮 ----------
function buildWheel(g, c) {
el('circle', { cx: c.x, cy: c.y, r: 80, fill: 'none', stroke: INK, 'stroke-width': 15 }, g);
el('circle', { cx: c.x, cy: c.y, r: 80, fill: 'none', stroke: '#34343a', 'stroke-width': 10 }, g);
el('circle', { cx: c.x, cy: c.y, r: 74.5, fill: 'none', stroke: '#56565f', 'stroke-width': 1.5 }, g);
el('circle', { cx: c.x, cy: c.y, r: 71, fill: 'none', stroke: '#c9d0d8', 'stroke-width': 3.5 }, g);
const spin = el('g', {}, g);
for (let i = 0; i < 20; i++) {
const a = i / 20 * Math.PI * 2, b = a + (i % 2 ? 0.32 : -0.32);
el('line', {
x1: c.x + Math.cos(a) * 8, y1: c.y + Math.sin(a) * 8,
x2: c.x + Math.cos(b) * 70, y2: c.y + Math.sin(b) * 70,
stroke: '#aeb7c1', 'stroke-width': 1.4
}, spin);
}
el('rect', { x: c.x + 38, y: c.y - 4, width: 13, height: 8, rx: 2, fill: '#ff9f1c', stroke: INK, 'stroke-width': 1.5 }, spin);
el('rect', { x: c.x - 3, y: c.y - 79, width: 6, height: 8, rx: 1.5, fill: '#9aa3ad' }, spin);
el('circle', { cx: c.x, cy: c.y, r: 9, fill: '#dfe4ea', stroke: INK, 'stroke-width': 3 }, g);
return spin;
}
const spinR = buildWheel($('wheelR'), RH);
const spinF = buildWheel($('wheelF'), FH);

// ---------- 视差平铺层 ----------
const layers = [];
function layer(id, period, factor, build, drift = 0) {
const g = $(id);
const n = Math.ceil(3600 / period) + 1;
for (let k = 0; k < n; k++) {
const tile = el('g', { transform: `translate(${-1200 + k * period},0)` }, g);
build(tile, rng(7919 + period));
}
layers.push({ g, period, factor, drift });
}

function cloud(p, x, y, s) {
const g = el('g', { transform: `translate(${x},${y}) scale(${s})` }, p);
el('ellipse', { cx: -4, cy: 20, rx: 72, ry: 11, fill: '#d3e9f7' }, g);
[[-44, 2, 26], [-12, -16, 36], [24, -6, 30], [52, 6, 20], [-64, 10, 16]]
.forEach(([cx, cy, r]) => el('circle', { cx, cy, r, fill: '#fff' }, g));
el('rect', { x: -72, y: 0, width: 132, height: 20, rx: 10, fill: '#fff' }, g);
}
layer('lyClouds', 1800, 0.05, (p, r) => {
for (let i = 0; i < 5; i++) cloud(p, r() * 1800, 40 + r() * 230, 0.55 + r() * 0.7);
}, 9);

layer('lyIsland', 3000, 0.02, p => {
const x = 380, lx = 392, b = 371;
el('path', { d: `M${x - 160},397 Q${x - 95},364 ${x - 20},370 Q${x + 55},358 ${x + 140},397 Z`, fill: '#8fbab3' }, p);
el('path', { d: `M${x - 110},397 Q${x - 60},381 ${x},385 Q${x + 42},379 ${x + 95},397 Z`, fill: '#77a69f' }, p);
el('path', { d: `M${lx},${b - 57} L${lx + 190},${b - 86} L${lx + 190},${b - 30} Z`, fill: '#fff7c2', class: 'beam' }, p);
el('path', { d: `M${lx - 9},${b} L${lx - 6},${b - 50} L${lx + 6},${b - 50} L${lx + 9},${b} Z`, fill: '#fff', stroke: '#5d6b7a', 'stroke-width': 1.2 }, p);
el('path', { d: `M${lx - 8.2},${b - 12} L${lx - 7.5},${b - 23} L${lx + 7.5},${b - 23} L${lx + 8.2},${b - 12} Z M${lx - 6.9},${b - 34} L${lx - 6.4},${b - 44} L${lx + 6.4},${b - 44} L${lx + 6.9},${b - 34} Z`, fill: '#e8483f' }, p);
el('rect', { x: lx - 6.5, y: b - 60, width: 13, height: 10, fill: '#ffe89a', stroke: '#5d6b7a', 'stroke-width': 1.2 }, p);
el('path', { d: `M${lx - 9},${b - 60} L${lx},${b - 68} L${lx + 9},${b - 60} Z`, fill: '#e8483f' }, p);
});

layer('lyBoat', 2200, 0.035, p => {
const x = 1350, y = 432;
const g = el('g', { class: 'bob' }, p);
el('path', { d: `M${x - 24},${y} L${x + 24},${y} L${x + 17},${y + 9} L${x - 17},${y + 9} Z`, fill: '#e8483f', stroke: INK, 'stroke-width': 1.8 }, g);
el('path', { d: `M${x - 2},${y - 4} L${x - 2},${y - 44} L${x - 22},${y - 5} Z`, fill: '#fff', stroke: INK, 'stroke-width': 1.8 }, g);
el('path', { d: `M${x + 2},${y - 4} L${x + 2},${y - 36} L${x + 19},${y - 5} Z`, fill: '#ffd35c', stroke: INK, 'stroke-width': 1.8 }, g);
}, -4);

layer('lySpark', 700, 0.08, (p, r) => {
for (let i = 0; i < 16; i++) {
el('rect', {
x: r() * 700, y: 402 + r() * 70, width: 5 + r() * 12, height: 2, rx: 1, fill: '#fff',
class: 'spark', style: `animation-delay:${(-r() * 2.6).toFixed(2)}s`
}, p);
}
for (let i = 0; i < 8; i++) {
const x = r() * 680, y = 415 + r() * 58;
el('path', { d: `M${x},${y} q5,-3 10,0 q5,-3 10,0`, fill: 'none', stroke: '#e6f6ff', 'stroke-width': 1.6, opacity: 0.55 }, p);
}
});

layer('lyFoam', 120, 0.3, p => {
el('path', { d: 'M0,481 q15,-7 30,0 t30,0 t30,0 t30,0 V489 H0 Z', fill: '#fff', opacity: 0.85 }, p);
});

function umbrella(p, x, y, c1, c2, tilt) {
const g = el('g', { transform: `translate(${x},${y}) rotate(${tilt})` }, p);
el('line', { x1: 0, y1: 0, x2: 0, y2: -84, stroke: '#6b4a2f', 'stroke-width': 3 }, g);
el('path', { d: 'M-46,-60 Q-42,-94 0,-96 Q42,-94 46,-60 Z', fill: c1, stroke: INK, 'stroke-width': 2 }, g);
el('path', { d: 'M-31,-60 L0,-96 L-11,-60 Z M11,-60 L0,-96 L31,-60 Z', fill: c2 }, g);
el('path', { d: 'M-46,-60 Q-42,-94 0,-96 Q42,-94 46,-60 Z', fill: 'none', stroke: INK, 'stroke-width': 2 }, g);
}
layer('lyBeach', 1400, 0.4, p => {
el('path', { d: 'M150,512 L232,512 L246,526 L164,526 Z', fill: '#ff7eb6' }, p);
el('path', { d: 'M170,512 L184,526 M196,512 L210,526', stroke: '#fff', 'stroke-width': 4 }, p);
umbrella(p, 200, 512, '#ff5a5f', '#fff', -6);
el('path', { d: 'M520,515 L520,494 L528,494 L528,500 L536,500 L536,490 L544,490 L544,500 L552,500 L552,494 L560,494 L560,515 Z', fill: '#e2bb78', stroke: '#b98d49', 'stroke-width': 1.5 }, p);
el('path', { d: 'M540,490 L540,476 L550,480 L540,484', fill: '#ff5a5f', stroke: '#b98d49', 'stroke-width': 1 }, p);
umbrella(p, 860, 510, '#2f7de1', '#ffd35c', 5);
const bx = 1080, by = 504;
el('circle', { cx: bx, cy: by, r: 11, fill: '#fff', stroke: INK, 'stroke-width': 1.8 }, p);
el('path', { d: `M${bx - 11},${by} A11,11 0 0 1 ${bx},${by - 11} L${bx},${by} Z M${bx + 11},${by} A11,11 0 0 1 ${bx},${by + 11} L${bx},${by} Z`, fill: '#ff5a5f' }, p);
});

layer('lyRail', 110, 0.85, p => {
el('rect', { x: 0, y: 497, width: 10, height: 56, rx: 2, fill: '#8a5c38', stroke: INK, 'stroke-width': 1.2 }, p);
el('rect', { x: -2, y: 493, width: 14, height: 6, rx: 2, fill: '#a8764b', stroke: INK, 'stroke-width': 1.2 }, p);
});

layer('lyPlanks', 36, 1, p => {
el('line', { x1: 0, y1: 556, x2: -12, y2: 636, stroke: '#9f6d42', 'stroke-width': 2 }, p);
el('line', { x1: 2.5, y1: 556, x2: -9.5, y2: 636, stroke: '#e6b37e', 'stroke-width': 1, opacity: 0.6 }, p);
});

layer('lyPiles', 150, 1.02, p => {
el('rect', { x: 0, y: 650, width: 16, height: 24, fill: '#6e4526' }, p);
el('ellipse', { cx: 8, cy: 675, rx: 14, ry: 3, fill: '#5b3a1e', opacity: 0.2 }, p);
});

layer('lyFg', 900, 1.35, (p, r) => {
for (let i = 0; i < 7; i++) {
const x = r() * 900, y = 668 + r() * 36, s = 0.7 + r() * 0.6;
el('path', {
d: `M${x},${y} q${-6 * s},${-16 * s} ${-15 * s},${-24 * s} M${x},${y} q0,${-20 * s} ${3 * s},${-34 * s} M${x},${y} q${7 * s},${-14 * s} ${16 * s},${-20 * s}`,
fill: 'none', stroke: i % 2 ? '#7fae5a' : '#6c9a49', 'stroke-width': 3.2, 'stroke-linecap': 'round'
}, p);
}
for (let i = 0; i < 4; i++) {
const x = r() * 900, y = 690 + r() * 30;
el('path', { d: `M${x},${y} l-9,-8 q9,-9 18,0 Z`, fill: '#ffd6c4', stroke: '#d59a86', 'stroke-width': 1.3 }, p);
}
});

// ---------- 海鸥 ----------
const gulls = [0, 1, 2].map(i => ({
el: el('path', { fill: 'none', stroke: '#3b4f63', 'stroke-width': 2.4, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, $('lyGulls')),
x: 150 + i * 480, y0: 90 + i * 55, s: 1 - i * 0.18, ph: i * 2.1
}));

// ---------- 特效:风线、扬沙 ----------
const fxG = $('fx');
const streaks = Array.from({ length: 9 }, () => ({
el: el('line', { stroke: '#fff', 'stroke-width': 2.2, 'stroke-linecap': 'round', opacity: 0 }, fxG),
x: -999, y: 0, len: 0
}));
const dust = Array.from({ length: 16 }, () => ({
el: el('circle', { r: 0, fill: '#e9c890', opacity: 0 }, fxG),
x: 0, y: 0, vx: 0, vy: 0, life: 0, max: 1
}));
let dustTimer = 0;

// ---------- 状态 ----------
const BASE_V = 250, RATIO = 0.62, LEG_A = 95, LEG_B = 102;
let speed = 1, targetSpeed = 1, dist = 0, wheelAng = 0, crankAng = -Math.PI / 2;
let chainOff = 0, time = 0, last = performance.now();
let nextBlink = 2, blinkT = -1, ringT = -1, audioCtx = null, shownOdo = -1;

const pel = $('pel'), dNeck = $('dNeck'), headParts = $('headParts');
const pouch = $('pouch'), eye = $('eye'), fishTail = $('fishTail');
const ringEl = $('ring'), chain = $('chain'), bell = $('bell'), bubble = $('bubble');
const scarfA = $('scarfA'), scarfB = $('scarfB');
const f1 = n => n.toFixed(1);

// 两段腿的反向运动学:已知髋和踝,求膝盖(膝盖朝前)
function knee(h, p) {
const dx = p.x - h.x, dy = p.y - h.y;
const d = Math.min(Math.max(Math.hypot(dx, dy), Math.abs(LEG_A - LEG_B) + 1), LEG_A + LEG_B - 0.5);
const k = Math.atan2(dy, dx) - Math.acos((LEG_A * LEG_A + d * d - LEG_B * LEG_B) / (2 * LEG_A * d));
return { x: h.x + LEG_A * Math.cos(k), y: h.y + LEG_A * Math.sin(k) };
}
function pose(side, hip, ang) {
const px = BB.x + CRANK * Math.cos(ang), py = BB.y + CRANK * Math.sin(ang);
const ank = { x: px - 5, y: py - 9 }, kn = knee(hip, ank);
const d = `M${f1(hip.x)},${f1(hip.y)} L${f1(kn.x)},${f1(kn.y)} L${f1(ank.x)},${f1(ank.y)}`;
$(side + 'LegO').setAttribute('d', d);
$(side + 'LegF').setAttribute('d', d);
$(side + 'Foot').setAttribute('transform', `translate(${f1(ank.x)},${f1(ank.y)}) rotate(${f1(6 + 14 * Math.sin(ang))})`);
const cranks = side === 'near' ? ['nearCrank', 'nearCrankO'] : ['farCrank'];
cranks.forEach(id => { $(id).setAttribute('x2', f1(px)); $(id).setAttribute('y2', f1(py)); });
const ped = $(side + 'Pedal');
ped.setAttribute('x', f1(px - 12));
ped.setAttribute('y', f1(py - 3.5));
}

// 围巾飘带:沿中心线叠加行进波
function ribbon(x0, y0, len, w, ph) {
const N = 14, top = [], bot = [];
const droop = 34 / (0.6 + speed), amp = 3 + 4 * Math.min(speed, 2.5), fr = 5 + 7 * speed;
for (let i = 0; i <= N; i++) {
const u = i / N, x = x0 - u * len * (0.75 + 0.1 * Math.min(speed, 2.5));
const y = y0 + droop * u * u + amp * u * Math.sin(u * 5.5 - time * fr + ph);
const hw = w * (1 - 0.3 * u);
top.push(`${f1(x)},${f1(y - hw)}`);
bot.unshift(`${f1(x)},${f1(y + hw)}`);
}
const [ex, ey] = top[N].split(',').map(Number);
return `M${top.join(' L')} L${f1(ex + 7)},${f1(ey + w * 0.7)} L${bot.join(' L')} Z`;
}

// ---------- 铃铛 ----------
function ding() {
try {
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
const t0 = audioCtx.currentTime;
[0, 0.17].forEach((delay, i) => {
[[2093, 0.2], [2637, 0.08], [4186, 0.04]].forEach(([f, vol]) => {
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = 'sine';
o.frequency.value = f * (i ? 1.03 : 1);
g.gain.setValueAtTime(0.0001, t0 + delay);
g.gain.exponentialRampToValueAtTime(vol, t0 + delay + 0.005);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + delay + 0.9);
o.connect(g).connect(audioCtx.destination);
o.start(t0 + delay);
o.stop(t0 + delay + 0.95);
});
});
} catch (e) { /* 无音频也照样动画 */ }
}
function ringBell() { ringT = 0; ding(); }

// ---------- 交互 ----------
const spd = $('spd'), spdVal = $('spdVal');
function setSpeed(v) {
targetSpeed = Math.max(0, Math.min(2.5, v));
spd.value = Math.round(targetSpeed * 100);
spdVal.textContent = targetSpeed.toFixed(1) + '×';
}
spd.addEventListener('input', () => setSpeed(spd.value / 100));
$('bellBtn').addEventListener('click', ringBell);
$('scene').addEventListener('click', ringBell);
addEventListener('keydown', e => {
if (e.code === 'Space') { e.preventDefault(); ringBell(); }
else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') setSpeed(targetSpeed + 0.2);
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') setSpeed(targetSpeed - 0.2);
});
if (matchMedia('(prefers-reduced-motion: reduce)').matches) { speed = 0.6; setSpeed(0.6); }

// 风线放在鹈鹕后面,避免划过角色
const backFx = el('g', {});
$('rig').before(backFx);
streaks.forEach(s => backFx.appendChild(s.el));
const odo = $('odo');
const easeBack = x => 1 + 2.70158 * Math.pow(x - 1, 3) + 1.70158 * Math.pow(x - 1, 2);

// ---------- 主循环 ----------
function frame(now) {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
time += dt;
speed += (targetSpeed - speed) * Math.min(1, dt * 3);
const v = BASE_V * speed, dW = v / WHEEL_R * dt, dC = dW * RATIO;
dist += v * dt;
wheelAng += dW;
crankAng += dC;
chainOff -= dC * 22;

// 视差背景
for (const L of layers) {
const off = ((dist * L.factor + time * L.drift) % L.period + L.period) % L.period;
L.g.setAttribute('transform', `translate(${(-off).toFixed(2)},0)`);
}

// 车轮、牙盘、链条
const wd = f1(wheelAng * 180 / Math.PI % 360);
spinR.setAttribute('transform', `rotate(${wd} ${RH.x} ${RH.y})`);
spinF.setAttribute('transform', `rotate(${wd} ${FH.x} ${FH.y})`);
ringEl.setAttribute('transform', `rotate(${f1(crankAng * 180 / Math.PI % 360)} ${BB.x} ${BB.y})`);
chain.setAttribute('stroke-dashoffset', (chainOff % 5).toFixed(2));

// 身体随踩踏起伏,两条腿跟着脚蹬走
const act = Math.min(1, speed);
const bob = 1.4 * Math.sin(2 * crankAng) * act;
pel.setAttribute('transform', `translate(0,${bob.toFixed(2)})`);
pose('far', { x: 556, y: 341 + bob }, crankAng + Math.PI);
pose('near', { x: 565, y: 345 + bob }, crankAng);

// 头颈:踩一下点一下头,骑得越快越往前探
const lean = Math.max(-3, Math.min(8, (speed - 1) * 5));
const nod = 2 * Math.sin(2 * crankAng + 0.9) * act + lean + 1.2 * Math.sin(time * 0.7);
const ht = `rotate(${nod.toFixed(2)} 630 275)`;
dNeck.setAttribute('transform', ht);
headParts.setAttribute('transform', ht);

// 按铃:铃铛摇晃、张嘴、冒气泡
let open = 0;
if (ringT >= 0) {
ringT += dt;
const w = Math.sin(ringT * 55) * 16 * Math.exp(-ringT * 4);
bell.setAttribute('transform', `rotate(${w.toFixed(2)} 701.5 352)`);
open = ringT < 0.8 ? Math.sin(Math.PI * ringT / 0.8) : 0;
const s = ringT < 0.22 ? easeBack(ringT / 0.22) : 1;
const op = ringT < 1.3 ? 1 : Math.max(0, 1 - (ringT - 1.3) / 0.3);
bubble.setAttribute('opacity', op.toFixed(2));
bubble.setAttribute('transform', `translate(716,${f1(342 - 8 * ringT)}) scale(${Math.max(0.01, s).toFixed(3)}) translate(-26,-72)`);
if (ringT > 1.6) {
ringT = -1;
bubble.setAttribute('opacity', 0);
bell.removeAttribute('transform');
}
}
const jig = 1 + 0.07 * Math.sin(2 * crankAng + 1.8) * act + 0.03 * Math.sin(time * 3.1);
pouch.setAttribute('transform', `rotate(${f1(open * 10)} 690 188) translate(750 196) scale(1 ${jig.toFixed(3)}) translate(-750 -196)`);

// 眨眼
nextBlink -= dt;
if (nextBlink <= 0) { blinkT = 0; nextBlink = 2.5 + Math.random() * 3; }
let ey = 1;
if (blinkT >= 0) {
blinkT += dt;
ey = Math.max(0.08, Math.abs(1 - blinkT / 0.07));
if (blinkT > 0.14) { blinkT = -1; ey = 1; }
}
eye.setAttribute('transform', `translate(675 168) scale(1 ${ey.toFixed(2)}) translate(-675 -168)`);

// 车筐里的鱼甩尾巴、围巾飘动
fishTail.setAttribute('transform', `rotate(${f1(Math.sin(time * 7) * 7)} 755 368)`);
scarfA.setAttribute('d', ribbon(601, 252, 96, 7, 0));
scarfB.setAttribute('d', ribbon(601, 257, 76, 6, 1.9));

// 海鸥
for (const g of gulls) {
g.x -= (v * 0.1 - 22) * dt;
if (g.x < -420) g.x += 2000; else if (g.x > 1580) g.x -= 2000;
const fl = Math.sin(time * 7 + g.ph), s = g.s, y = g.y0 + 8 * Math.sin(time * 0.9 + g.ph);
const wy = (-5 - 7 * fl) * s;
g.el.setAttribute('d', `M${f1(g.x - 14 * s)},${f1(y + 2 * s)} Q${f1(g.x - 7 * s)},${f1(y + wy)} ${f1(g.x)},${f1(y)} Q${f1(g.x + 7 * s)},${f1(y + wy)} ${f1(g.x + 14 * s)},${f1(y + 2 * s)}`);
}

// 风线:速度越快越明显
const sOp = Math.max(0, Math.min(0.75, (speed - 0.9) / 1.2)).toFixed(2);
for (const s of streaks) {
s.x -= v * 2.4 * dt;
if (s.x + s.len < -300) {
s.x = 900 + Math.random() * 700;
s.y = 110 + Math.random() * 380;
s.len = 40 + Math.random() * 90;
}
s.el.setAttribute('x1', f1(s.x));
s.el.setAttribute('x2', f1(s.x + s.len));
s.el.setAttribute('y1', f1(s.y));
s.el.setAttribute('y2', f1(s.y));
s.el.setAttribute('opacity', sOp);
}

// 后轮扬起的沙粒
dustTimer -= dt * speed;
if (speed > 0.15 && dustTimer <= 0) {
dustTimer = 0.09;
const p = dust.find(d => d.life <= 0);
if (p) {
p.x = RH.x - 32 + Math.random() * 12;
p.y = 578;
p.vx = -(30 + Math.random() * 50) - v * 0.3;
p.vy = -(12 + Math.random() * 22);
p.max = p.life = 0.6 + Math.random() * 0.4;
}
}
for (const p of dust) {
if (p.life <= 0) continue;
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 25 * dt;
const k = Math.min(1, 1 - p.life / p.max);
p.el.setAttribute('cx', f1(p.x));
p.el.setAttribute('cy', f1(p.y));
p.el.setAttribute('r', f1(3 + 7 * k));
p.el.setAttribute('opacity', p.life > 0 ? (0.5 * (1 - k)).toFixed(2) : 0);
}

// 里程
const m = Math.floor(dist / 100);
if (m !== shownOdo) { shownOdo = m; odo.textContent = m; }

requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
最新回复 (7)
  • coolb 09-23 01:54
    1

    铁汁,能不能直接上图。。。。

  • 隔壁老陈 楼主 09-23 01:54
    2

    emmmm不是说了吗,效果真的不是截图能感受到的,懒得做动图了。

  • Myth 09-23 01:55
    3

    neo啥时候支持个html站内渲染

  • 隔壁老陈 楼主 09-23 01:55
    4

    别说了,我本来想直接把html文件发上来,结果,还不让上传。

  • 隔壁老陈 楼主 09-23 01:57
    5

    也可以直接去看哈雷佬打包的html附件,震惊围坐 Opus 5.5

  • 哈雷彗星 09-23 01:59
    6

    发到 CodePen 就可以嵌 iframe


    不做不可信站点的渲染是因为 安全

  • Myth 09-23 02:27
    7

    有没有成功案例的参考啊

    试着嵌入了2.0和classic都不生效

* 帖子来源Linux.do
返回