鹈鹕骑自行车 Antigravity IDE Gemini 3.8 Flash High 这么强的吗?
Cornelius
2026-09-12 00:25
1

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>鹈鹕骑行记 - 2D SVG 动画</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-gradient: linear-gradient(180deg, #7ad5f5 0%, #caeff9 55%, #ffe6ab 100%);
--sea-color: #279af1;
--sea-deep: #1665a3;
--road-color: #3b4252;
--road-line: #f4d03f;
--glass-bg: rgba(255, 255, 255, 0.75);
--glass-border: rgba(255, 255, 255, 0.4);
--glass-shadow: 0 12px 36px rgba(15, 35, 60, 0.15);
--text-main: #1f2937;
--text-sub: #4b5563;
--accent: #ff6b35;
--accent-hover: #fa5219;
--btn-bg: #ffffff;
--btn-active: #ff6b35;
--btn-active-text: #ffffff;
}
[data-theme="sunset"] {
--bg-gradient: linear-gradient(180deg, #35265e 0%, #b84362 42%, #ff7b54 75%, #ffb26b 100%);
--sea-color: #583366;
--sea-deep: #2e173e;
--road-color: #2b2d42;
--road-line: #ffae58;
--glass-bg: rgba(45, 35, 60, 0.78);
--glass-border: rgba(255, 180, 150, 0.25);
--glass-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
--text-main: #ffffff;
--text-sub: #ffd1bd;
--accent: #ff8c42;
--accent-hover: #ffaa6c;
--btn-bg: rgba(255, 255, 255, 0.15);
--btn-active: #ff8c42;
--btn-active-text: #ffffff;
}
[data-theme="night"] {
--bg-gradient: linear-gradient(180deg, #090c15 0%, #10192e 50%, #1e2949 100%);
--sea-color: #0b172a;
--sea-deep: #050b14;
--road-color: #171b26;
--road-line: #f1c40f;
--glass-bg: rgba(16, 25, 46, 0.85);
--glass-border: rgba(255, 255, 255, 0.12);
--glass-shadow: 0 12px 36px rgba(0, 0, 0, 0.5);
--text-main: #f3f4f6;
--text-sub: #9ca3af;
--accent: #00d2d3;
--accent-hover: #01a3a4;
--btn-bg: rgba(255, 255, 255, 0.1);
--btn-active: #00d2d3;
--btn-active-text: #0b172a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
font-family: 'Outfit', 'Noto Sans SC', sans-serif;
background: #0f172a;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
color: var(--text-main);
padding: 12px;
}
.main-wrapper {
position: relative;
width: 100%;
max-width: 1200px;
aspect-ratio: 16 / 9;
min-height: 380px;
border-radius: 28px;
overflow: hidden;
box-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
background: var(--bg-gradient);
transition: background 0.8s ease;
}
@media (max-width: 768px) {
.main-wrapper {
aspect-ratio: 4 / 3;
border-radius: 20px;
}
}
svg#stage {
width: 100%;
height: 100%;
display: block;
overflow: hidden;
}
/* UI Overlay Header */
.title-banner {
position: absolute;
top: 18px;
left: 24px;
z-index: 10;
pointer-events: none;
}
.title-banner h1 {
font-size: clamp(1.1rem, 2.8vw, 1.8rem);
font-weight: 800;
letter-spacing: -0.5px;
color: var(--text-main);
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
gap: 8px;
transition: color 0.5s ease;
}
.title-banner p {
font-size: clamp(0.75rem, 1.4vw, 0.95rem);
color: var(--text-sub);
margin-top: 2px;
font-weight: 500;
transition: color 0.5s ease;
}
.badge {
display: inline-block;
font-size: 0.65rem;
padding: 2px 8px;
background: var(--accent);
color: white;
border-radius: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
vertical-align: middle;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}
/* Floating Control Deck */
.control-panel {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 36px);
max-width: 820px;
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 12px 20px;
box-shadow: var(--glass-shadow);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
z-index: 20;
transition: all 0.5s ease;
flex-wrap: wrap;
}
@media (max-width: 680px) {
.control-panel {
bottom: 12px;
padding: 10px 14px;
gap: 8px;
border-radius: 16px;
}
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.btn {
appearance: none;
border: none;
outline: none;
cursor: pointer;
background: var(--btn-bg);
color: var(--text-main);
font-family: inherit;
font-weight: 600;
font-size: 0.85rem;
padding: 8px 14px;
border-radius: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16);
background: #ffffff;
color: #000000;
}
.btn:active {
transform: translateY(1px);
}
.btn.active {
background: var(--btn-active);
color: var(--btn-active-text);
box-shadow: 0 4px 14px rgba(255, 107, 53, 0.35);
}
.btn-icon {
width: 38px;
height: 38px;
padding: 0;
border-radius: 50%;
font-size: 1.1rem;
}
.bell-btn {
background: linear-gradient(135deg, #ffd700, #ff9f1a);
color: #523600;
font-weight: 700;
animation: bell-pulse 2s infinite ease-in-out;
}
@keyframes bell-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
.speed-slider-wrap {
display: flex;
align-items: center;
gap: 8px;
background: var(--btn-bg);
padding: 6px 14px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.speed-label {
font-size: 0.78rem;
font-weight: 700;
color: var(--text-sub);
min-width: 58px;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 90px;
height: 6px;
border-radius: 3px;
background: rgba(120, 120, 120, 0.3);
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);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: transform 0.15s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.25);
}
/* Floating Bubble / Interactive Hints */
.bubble-tip {
position: absolute;
top: 18px;
right: 24px;
background: var(--glass-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
padding: 6px 14px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-main);
box-shadow: var(--glass-shadow);
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: all 0.25s ease;
z-index: 10;
}
.bubble-tip:hover {
transform: scale(1.04);
background: #ffffff;
color: #111;
}
/* Ding Visual Effect Wave */
.ding-effect {
position: absolute;
pointer-events: none;
font-weight: 800;
font-size: 1.2rem;
color: #ff9800;
text-shadow: 0 2px 8px rgba(255, 255, 255, 0.8);
animation: float-fade 1s forwards ease-out;
z-index: 30;
}
@keyframes float-fade {
0% { opacity: 1; transform: translate(-50%, 0) scale(0.8); }
100% { opacity: 0; transform: translate(-50%, -60px) scale(1.4); }
}
</style>
</head>
<body data-theme="day">
<div class="main-wrapper" id="canvasContainer">
<!-- Title Info -->
<div class="title-banner">
<h1>
鹈鹕骑行日记 <span class="badge">SVG 2D</span>
</h1>
<p>吹着海风兜风的快乐大鸟 🌊🚴♂️</p>
</div>
<!-- Quick Tip & Sound toggle -->
<div class="bubble-tip" id="soundBtn" title="点击开关音效">
<span id="soundIcon">🔊</span> <span id="soundText">音效开</span>
</div>
<!-- MAIN SVG STAGE -->
<svg id="stage" viewBox="0 0 1200 675" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Gradients -->
<linearGradient id="sunGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fff9d2" />
<stop offset="40%" stop-color="#ffcc00" />
<stop offset="100%" stop-color="#ff7b25" />
</linearGradient>
<linearGradient id="cloudGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
<stop offset="100%" stop-color="#e8f4f8" stop-opacity="0.75" />
</linearGradient>
<linearGradient id="hillGrad1" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#5fa8d3" stop-opacity="0.5" />
<stop offset="100%" stop-color="#1b4965" stop-opacity="0.7" />
</linearGradient>
<linearGradient id="hillGrad2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#62b6cb" stop-opacity="0.8" />
<stop offset="100%" stop-color="#1b4965" stop-opacity="0.95" />
</linearGradient>
<linearGradient id="seaGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#48cae4" />
<stop offset="35%" stop-color="#0096c7" />
<stop offset="100%" stop-color="#023e8a" />
</linearGradient>
<!-- Pelican Gradients -->
<linearGradient id="pelicanBodyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="70%" stop-color="#f2f5f8" />
<stop offset="100%" stop-color="#dce3eb" />
</linearGradient>
<linearGradient id="pouchGrad" x1="0%" y1="0%" x2="100%" y2="80%">
<stop offset="0%" stop-color="#ffa94d" />
<stop offset="45%" stop-color="#ff7a59" />
<stop offset="100%" stop-color="#ea495f" />
</linearGradient>
<linearGradient id="beakTopGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ffd166" />
<stop offset="85%" stop-color="#ff9f1c" />
<stop offset="100%" stop-color="#f77f00" />
</linearGradient>
<linearGradient id="bikeFrameGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06d6a0" />
<stop offset="50%" stop-color="#118ab2" />
<stop offset="100%" stop-color="#073b4c" />
</linearGradient>
<linearGradient id="chromeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="40%" stop-color="#d6dadf" />
<stop offset="70%" stop-color="#9aa0a6" />
<stop offset="100%" stop-color="#f1f3f4" />
</linearGradient>
<linearGradient id="leatherGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#a0522d" />
<stop offset="100%" stop-color="#5c2c16" />
</linearGradient>
<linearGradient id="headlightBeam" x1="0%" y1="50%" x2="100%" y2="50%">
<stop offset="0%" stop-color="#fff8db" stop-opacity="0.85" />
<stop offset="25%" stop-color="#ffeaa7" stop-opacity="0.5" />
<stop offset="100%" stop-color="#ffeaa7" stop-opacity="0" />
</linearGradient>
<!-- Drop Shadow Filters -->
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-opacity="0.18" />
</filter>
<filter id="softGlow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- 1. SKY & CELESTIAL BODIES -->
<g id="skyGroup">
<!-- Sun / Moon -->
<circle id="celestialBody" cx="960" cy="130" r="46" fill="url(#sunGrad)" filter="url(#softGlow)" />
<!-- Clouds Layer (Parallax 1) -->
<g id="cloudLayer">
<!-- Cloud 1 -->
<path class="cloud" d="M100 120 a30 30 0 0 1 50 -10 a45 45 0 0 1 70 5 a25 25 0 0 1 35 25 a20 20 0 0 1 -15 20 l-130 0 a20 20 0 0 1 -10 -40 z" fill="url(#cloudGrad)" opacity="0.85" />
<!-- Cloud 2 -->
<path class="cloud" d="M520 80 a25 25 0 0 1 40 -8 a38 38 0 0 1 60 5 a20 20 0 0 1 30 20 l-120 0 a18 18 0 0 1 -10 -17 z" fill="url(#cloudGrad)" opacity="0.75" />
<!-- Cloud 3 -->
<path class="cloud" d="M850 150 a28 28 0 0 1 45 -10 a40 40 0 0 1 65 6 a22 22 0 0 1 30 22 l-130 0 a18 18 0 0 1 -10 -18 z" fill="url(#cloudGrad)" opacity="0.7" />
</g>
</g>
<!-- 2. DISTANT COASTAL HILLS & LIGHTHOUSE -->
<g id="sceneryDistant">
<!-- Far Hills -->
<path d="M-50 360 Q150 260 380 320 T800 280 T1250 330 L1250 430 L-50 430 Z" fill="url(#hillGrad1)" />
<!-- Lighthouse -->
<g transform="translate(920, 245) scale(0.65)">
<polygon points="20,80 30,10 42,10 52,80" fill="#f8f9fa" stroke="#333" stroke-width="1.5" />
<!-- Stripes -->
<polygon points="23,60 49,60 46,45 26,45" fill="#e63946" />
<polygon points="28,30 44,30 42,20 30,20" fill="#e63946" />
<!-- Light chamber & dome -->
<rect x="28" y="0" width="16" height="12" fill="#ffd166" />
<path d="M26 0 Q36 -12 46 0 Z" fill="#1d3557" />
</g>
<!-- Nearer Hills with Windmills -->
<path d="M-40 380 Q200 310 500 355 T1000 330 T1260 375 L1260 440 L-40 440 Z" fill="url(#hillGrad2)" />
<!-- Tiny Sailing Boats -->
<g id="sailBoat1" transform="translate(300, 375) scale(0.7)">
<path d="M0 15 L25 15 L20 22 L5 22 Z" fill="#ffffff" />
<polygon points="12,14 12,0 24,14" fill="#ff4d6d" />
</g>
</g>
<!-- 3. OCEAN WITH ANIMATED WAVES -->
<g id="oceanGroup">
<rect x="0" y="390" width="1200" height="90" fill="url(#seaGrad)" />
<g id="waveLines" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round" fill="none">
<path d="M20 410 q30 -6 60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0 t60 0" />
<path d="M-10 435 q35 -5 70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0 t70 0" />
<path d="M15 460 q40 -6 80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0 t80 0" />
</g>
</g>
<!-- 4. FOREGROUND COASTAL PROMENADE / ROAD -->
<g id="roadGroup">
<!-- Promenade Sea Wall / Guardrail -->
<rect x="0" y="465" width="1200" height="15" fill="#a0aec0" />
<line x1="0" y1="465" x2="1200" y2="465" stroke="#cbd5e1" stroke-width="2.5" />
<!-- Fence Posts moving with parallax -->
<g id="fenceGroup" stroke="#475569" stroke-width="4">
<!-- Railing top pipe -->
<line x1="0" y1="450" x2="1200" y2="450" stroke="#64748b" stroke-width="5" />
<!-- Posts (rendered/shifted via JS) -->
<g id="fencePosts"></g>
</g>
<!-- Road Base -->
<rect x="0" y="480" width="1200" height="195" fill="var(--road-color)" />
<!-- Road Shoulder curb -->
<rect x="0" y="480" width="1200" height="8" fill="#e2e8f0" />
<rect x="0" y="488" width="1200" height="6" fill="#718096" />
<!-- Road Dashed Centerlines (Moving) -->
<g id="roadLinesGroup">
<line id="roadLines" x1="0" y1="575" x2="1200" y2="575"
stroke="var(--road-line)" stroke-width="7" stroke-dasharray="55, 45" />
</g>
<!-- Roadside Grass & Flower Tufts -->
<g id="roadsideFoliage"></g>
</g>
<!-- 5. BICYCLE & PELICAN ACTOR (Central Rigs) -->
<g id="actorRoot" transform="translate(0, 0)">
<!-- Ground Shadow -->
<ellipse id="groundShadow" cx="590" cy="564" rx="195" ry="16" fill="rgba(10, 20, 30, 0.32)" />
<!-- Speed Wind Lines (Behind) -->
<g id="speedLinesBack" stroke="rgba(255,255,255,0.7)" stroke-linecap="round" stroke-width="3"></g>
<!-- Left Pedal & Crank Arm (Behind bike) -->
<g id="crankBackGroup">
<line id="crankArmLeft" x1="550" y1="480" x2="525" y2="505" stroke="#718096" stroke-width="7" stroke-linecap="round" />
<rect id="pedalLeft" x="515" y="501" width="22" height="8" rx="3" fill="#2d3748" />
</g>
<!-- Pelican Left Leg (Far side) -->
<path id="pelicanLegLeft" d="M510 320 L515 420 L525 505"
fill="none" stroke="#e07a5f" stroke-width="11" stroke-linecap="round" stroke-linejoin="round" opacity="0.85" />
<!-- Webbed Foot Left -->
<polygon id="pelicanFootLeft" points="525,505 540,510 515,510" fill="#d95d39" opacity="0.85" />
<!-- REAR WHEEL (Center: 410, 485) -->
<g id="wheelRear" transform="translate(410, 485)">
<!-- Outer Tire -->
<circle cx="0" cy="0" r="72" fill="none" stroke="#1f242d" stroke-width="14" />
<circle cx="0" cy="0" r="77" fill="none" stroke="#333a46" stroke-width="2" stroke-dasharray="4, 3" />
<!-- Inner Rim -->
<circle cx="0" cy="0" r="64" fill="none" stroke="url(#chromeGrad)" stroke-width="5" />
<!-- Spokes Group (will rotate) -->
<g id="spokesRear" stroke="#cbd5e1" stroke-width="1.8" opacity="0.85"></g>
<!-- Center Hub -->
<circle cx="0" cy="0" r="11" fill="#475569" stroke="#cbd5e1" stroke-width="2" />
<!-- Cassette/Gear Cog -->
<circle cx="0" cy="0" r="22" fill="none" stroke="#94a3b8" stroke-width="4" stroke-dasharray="6, 3" />
</g>
<!-- FRONT WHEEL (Center: 750, 485) -->
<g id="wheelFront" transform="translate(750, 485)">
<!-- Outer Tire -->
<circle cx="0" cy="0" r="72" fill="none" stroke="#1f242d" stroke-width="14" />
<circle cx="0" cy="0" r="77" fill="none" stroke="#333a46" stroke-width="2" stroke-dasharray="4, 3" />
<!-- Inner Rim -->
<circle cx="0" cy="0" r="64" fill="none" stroke="url(#chromeGrad)" stroke-width="5" />
<!-- Spokes Group -->
<g id="spokesFront" stroke="#cbd5e1" stroke-width="1.8" opacity="0.85"></g>
<!-- Center Hub -->
<circle cx="0" cy="0" r="11" fill="#475569" stroke="#cbd5e1" stroke-width="2" />
</g>
<!-- BICYCLE FRAME & COMPONENTS -->
<g id="bikeFrameGroup">
<!-- Chainstay (Rear hub 410,485 to Bottom Bracket 550,485) -->
<line x1="410" y1="485" x2="550" y2="485" stroke="url(#bikeFrameGrad)" stroke-width="8" stroke-linecap="round" />
<!-- Seatstay (Rear hub 410,485 to Seat Lug 490,345) -->
<line x1="410" y1="485" x2="490" y2="345" stroke="url(#bikeFrameGrad)" stroke-width="7" stroke-linecap="round" />
<!-- Seat Tube (Bottom Bracket 550,485 to Seat Lug 490,345) -->
<line x1="550" y1="485" x2="490" y2="345" stroke="url(#bikeFrameGrad)" stroke-width="9" stroke-linecap="round" />
<!-- Seat Post & Saddle -->
<line x1="490" y1="345" x2="475" y2="310" stroke="url(#chromeGrad)" stroke-width="7" stroke-linecap="round" />
<!-- Vintage Sprung Saddle -->
<g id="saddle" transform="translate(470, 302)">
<!-- Springs -->
<path d="M-8 8 Q-12 14 -4 16 T0 20" fill="none" stroke="#475569" stroke-width="2.5" />
<path d="M8 8 Q4 14 12 16 T16 20" fill="none" stroke="#475569" stroke-width="2.5" />
<!-- Saddle Leather Cushion -->
<path d="M-22 4 C-24 -4 35 -6 42 6 C38 12 10 10 -2 8 C-10 8 -18 10 -22 4 Z" fill="url(#leatherGrad)" stroke="#3e1a0b" stroke-width="1.5" />
<!-- Chrome Rivets -->
<circle cx="-16" cy="3" r="1.5" fill="#f8fafc" />
<circle cx="-8" cy="2" r="1.5" fill="#f8fafc" />
<circle cx="0" cy="2" r="1.5" fill="#f8fafc" />
</g>
<!-- Down Tube (Bottom Bracket 550,485 to Head Tube 690,330) -->
<line x1="550" y1="485" x2="690" y2="330" stroke="url(#bikeFrameGrad)" stroke-width="10" stroke-linecap="round" />
<!-- Top Tube (Seat Lug 490,345 to Head Tube 690,330) -->
<line x1="490" y1="345" x2="690" y2="330" stroke="url(#bikeFrameGrad)" stroke-width="9" stroke-linecap="round" />
<!-- Front Fork (Head Tube 690,330 down to Front Hub 750,485) -->
<line x1="695" y1="340" x2="750" y2="485" stroke="url(#bikeFrameGrad)" stroke-width="8" stroke-linecap="round" />
<!-- Fenders / Mudguards (Retro curved) -->
<!-- Rear Fender -->
<path d="M340 485 A76 76 0 0 1 480 435" fill="none" stroke="#06d6a0" stroke-width="6" stroke-linecap="round" />
<!-- Front Fender -->
<path d="M720 415 A76 76 0 0 1 815 480" fill="none" stroke="#06d6a0" stroke-width="6" stroke-linecap="round" />
<!-- Chain & Chainring -->
<!-- Bicycle Chain loop -->
<path d="M410 477 L550 467 A24 24 0 0 1 550 503 L410 493 A10 10 0 0 1 410 477 Z"
fill="none" stroke="#64748b" stroke-width="3" stroke-dasharray="5, 3" />
<!-- Large Front Chainring -->
<circle cx="550" cy="485" r="26" fill="#1e293b" stroke="url(#chromeGrad)" stroke-width="4" />
<circle cx="550" cy="485" r="16" fill="none" stroke="#64748b" stroke-width="2" stroke-dasharray="4, 4" />
<!-- Handlebar Stem & Swept-back Cruiser Bars -->
<line x1="690" y1="330" x2="680" y2="280" stroke="url(#chromeGrad)" stroke-width="7" stroke-linecap="round" />
<!-- Handlebars curve -->
<path d="M680 280 C680 260 655 260 645 272" fill="none" stroke="url(#chromeGrad)" stroke-width="6" stroke-linecap="round" />
<!-- Grip -->
<path d="M652 268 L642 276" stroke="#4a2810" stroke-width="8" stroke-linecap="round" />
<!-- Brass Bicycle Bell (Clickable!) -->
<g id="bikeBell" transform="translate(670, 255)" style="cursor: pointer;">
<path d="M-8 6 C-8 -4 8 -4 8 6 Z" fill="url(#sunGrad)" stroke="#b45309" stroke-width="1.2" />
<circle cx="0" cy="-2" r="2.5" fill="#fef08a" />
<rect x="-2" y="6" width="4" height="3" fill="#78350f" />
</g>
<!-- Headlight with night beam -->
<g id="headlightGroup" transform="translate(710, 315)">
<!-- Light Beam (visible at night or on demand) -->
<polygon id="lightBeam" points="14,-5 290,-50 310,130 14,15" fill="url(#headlightBeam)" opacity="0" />
<!-- Lamp Casing -->
<path d="M-4 -8 C-8 -8 -12 -4 -12 4 C-12 12 -8 16 -4 16 L10 14 L12 -6 Z" fill="#334155" stroke="url(#chromeGrad)" stroke-width="1.5" />
<!-- Lamp Glass Face -->
<ellipse cx="12" cy="4" rx="3" ry="10" fill="#fef08a" stroke="#ffffff" stroke-width="1" />
</g>
</g>
<!-- 6. PELICAN CHARACTER (Mounted & Animated) -->
<g id="pelicanRig">
<!-- Tail Feathers (Wobbles with movement) -->
<g id="pelicanTail" transform="translate(425, 310)">
<path d="M0 0 C-25 -10 -40 -5 -45 10 C-35 15 -15 8 0 0 Z" fill="#2d3748" />
<path d="M0 4 C-20 -2 -35 6 -40 18 C-28 20 -10 12 0 4 Z" fill="#4a5568" />
<path d="M0 8 C-15 8 -25 18 -30 26 C-18 24 -5 16 0 8 Z" fill="#cbd5e1" />
</g>
<!-- Main Torso / Body -->
<!-- Pelican body curves forward naturally from saddle to chest -->
<path id="pelicanTorso" d="M445 320
C425 285 460 230 520 230
C570 230 605 255 615 285
C625 320 580 355 520 355
C470 355 450 340 445 320 Z"
fill="url(#pelicanBodyGrad)" filter="url(#shadow)" />
<!-- Back Flapping Scarf End 1 & 2 (Flowing in wind) -->
<path id="scarfTailBack" d="M570 220 Q510 200 460 215 T400 210"
fill="none" stroke="#e63946" stroke-width="10" stroke-linecap="round" />
<path id="scarfTailFront" d="M575 224 Q520 212 475 230 T415 228"
fill="none" stroke="#ff4d6d" stroke-width="8" stroke-linecap="round" />
<!-- Pelican Neck -->
<path id="pelicanNeck" d="M565 240
C575 210 590 170 635 155
C650 150 670 170 660 195
C645 225 615 260 595 275 Z"
fill="url(#pelicanBodyGrad)" />
<!-- Pelican Head Group (Nods & bounces) -->
<g id="pelicanHead" transform="translate(640, 160)">
<!-- Back Tuft / Crest -->
<path d="M-22 -8 C-36 -16 -42 -8 -45 2 C-38 6 -28 2 -20 0 Z" fill="#e2e8f0" />
<path d="M-20 -14 C-32 -26 -38 -20 -40 -10 C-32 -6 -24 -6 -18 -8 Z" fill="#cbd5e1" />
<!-- Head Base Skull -->
<ellipse cx="0" cy="0" rx="28" ry="24" fill="url(#pelicanBodyGrad)" />
<!-- Cheek Blush -->
<circle cx="8" cy="10" r="9" fill="#ff87ab" opacity="0.45" />
<!-- GULAR THROAT POUCH (The famous pelican pouch!) -->
<!-- Elastic springy pouch that bounces -->
<g id="throatPouchGroup">
<path id="throatPouch" d="M12 8
C25 25 35 70 20 85
C5 98 -25 90 -30 65
C-35 45 -25 22 -15 12 Z"
fill="url(#pouchGrad)" stroke="#d94858" stroke-width="1.8" />
<!-- Pouch texture veins / folds -->
<path d="M10 20 Q18 50 10 75" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" />
<path d="M-5 18 Q2 55 -8 72" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" />
<!-- CUTE LITTLE FISH PEEKING OUT OF POUCH! -->
<g id="littleFish" transform="translate(8, 22) rotate(-15)">
<!-- Fish Body -->
<ellipse cx="0" cy="0" rx="14" ry="8" fill="#2ec4b6" />
<path d="M-12 0 L-22 -8 L-20 0 L-22 8 Z" fill="#0cb0a0" />
<!-- Fish Belly -->
<path d="M-10 2 Q0 8 10 2 Z" fill="#cbf3f0" />
<!-- Fish Eye (Surprised 'o') -->
<circle cx="6" cy="-2" r="3.5" fill="#ffffff" />
<circle cx="7" cy="-2" r="1.8" fill="#111111" />
<!-- Fish Sunglasses (Super cool) -->
<rect x="2" y="-5" width="8" height="5" rx="1" fill="#1e293b" />
<line x1="2" y1="-3" x2="-2" y2="-1" stroke="#1e293b" stroke-width="1" />
<!-- Water drop bubble -->
<circle cx="12" cy="-10" r="2.5" fill="rgba(255,255,255,0.7)" />
</g>
</g>
<!-- UPPER BEAK / BILL (Massive & distinctive) -->
<path id="upperBeak" d="M10 -8
L110 -2
C118 0 120 8 114 12
L15 14
C5 12 0 4 10 -8 Z"
fill="url(#beakTopGrad)" stroke="#d97706" stroke-width="1.5" />
<!-- Beak Ridge & Hook Tip -->
<path d="M10 -8 L112 -1 C116 2 116 8 114 11" fill="none" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" opacity="0.7" />
<path d="M112 0 C117 3 118 8 113 12" fill="#d97706" />
<!-- Nostril slit -->
<line x1="25" y1="-2" x2="35" y2="-1" stroke="#92400e" stroke-width="1.5" stroke-linecap="round" />
<!-- BIG FRIENDLY EYE -->
<g id="pelicanEye" transform="translate(4, -6)">
<!-- Sclera -->
<ellipse cx="0" cy="0" rx="9" ry="11" fill="#ffffff" stroke="#cbd5e1" stroke-width="0.8" />
<!-- Pupil looking excitedly forward-down -->
<ellipse id="pupil" cx="2.5" cy="1" rx="5" ry="6" fill="#1e293b" />
<!-- Catchlight / Highlights -->
<circle cx="4" cy="-2" r="2.2" fill="#ffffff" />
<circle cx="1" cy="3" r="1.2" fill="#ffffff" />
<!-- Eyelid (for blinking) -->
<path id="eyelid" d="M-9 -11 Q0 -12 9 -11 L9 -11 Q0 -11 -9 -11 Z" fill="#ffd166" />
</g>
<!-- VINTAGE CYCLING CAP -->
<g id="cyclingCap" transform="translate(-2, -18) rotate(-8)">
<!-- Cap Dome -->
<path d="M-26 2 C-26 -18 20 -20 28 2 Z" fill="#1d3557" />
<!-- White & Red Racing Stripes -->
<path d="M-10 -16 C-2 -18 8 -18 14 -16 L12 2 L-8 2 Z" fill="#f1faee" />
<path d="M-2 -17 L4 -17 L2 2 L-4 2 Z" fill="#e63946" />
<!-- Cap Brim (Flipped up retro style) -->
<path d="M24 0 C38 -4 44 8 36 12 C28 8 24 2 24 0 Z" fill="#e63946" stroke="#b71c1c" stroke-width="1" />
<circle cx="0" cy="-18" r="3" fill="#ffd166" />
</g>
</g> <!-- End Pelican Head -->
<!-- Cozy Red Scarf Knot on Neck -->
<g id="scarfKnot" transform="translate(615, 230)">
<ellipse cx="0" cy="0" rx="14" ry="10" fill="#e63946" stroke="#b71c1c" stroke-width="1.2" />
<ellipse cx="-4" cy="1" rx="9" ry="7" fill="#ff4d6d" />
</g>
<!-- Pelican WING / ARM (Resting confidently on handlebar grip) -->
<g id="pelicanWing">
<!-- Wing Base connected to shoulder (510, 260) extending to grip (650, 270) -->
<path d="M470 295
C450 250 510 230 560 240
C610 250 635 260 655 270
C640 280 610 280 570 290
C530 300 490 325 470 295 Z"
fill="url(#pelicanBodyGrad)" stroke="#cbd5e1" stroke-width="1.5" />
<!-- Wing Primary Feathers Layer -->
<path d="M520 270 C560 270 610 275 645 272 C620 290 580 305 520 295 Z" fill="#94a3b8" opacity="0.6" />
<!-- Wingtip Feathers wrapped around handlebar -->
<path d="M642 266 C656 264 662 272 654 278 C646 280 640 274 642 266 Z" fill="#64748b" />
</g>
<!-- Pelican RIGHT LEG & FOOT (Near side, fully pedaling!) -->
<path id="pelicanLegRight" d="M515 325 L545 425 L575 480"
fill="none" stroke="#f77f00" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" />
<!-- Knee joint highlight -->
<circle id="kneeRight" cx="545" cy="425" r="7" fill="#f77f00" />
<!-- Webbed Foot Right on Pedal -->
<polygon id="pelicanFootRight" points="575,480 600,488 565,490" fill="#d97706" />
<!-- Right Crank & Pedal (Near side) -->
<g id="crankFrontGroup">
<line id="crankArmRight" x1="550" y1="485" x2="575" y2="480" stroke="url(#chromeGrad)" stroke-width="8" stroke-linecap="round" />
<circle cx="550" cy="485" r="7" fill="#334155" />
<rect id="pedalRight" x="565" y="475" width="24" height="9" rx="3" fill="#1e293b" stroke="#64748b" stroke-width="1" />
</g>
</g> <!-- End Pelican Rig -->
<!-- FRONT SPEED STREAKS & ROAD DUST -->
<g id="speedLinesFront" stroke="rgba(255,255,255,0.85)" stroke-linecap="round" stroke-width="2.5"></g>
<!-- Tire Puff Dust Particles -->
<g id="dustParticles"></g>
</g> <!-- End Actor Root -->
</svg>
<!-- FLOATING CONTROL DOCK (Responsive & Accessible) -->
<div class="control-panel">
<!-- Play / Pause -->
<div class="control-group">
<button class="btn btn-icon" id="playPauseBtn" title="播放/暂停">
<span id="playIcon">⏸️</span>
</button>
<button class="btn bell-btn" id="ringBellBtn" title="按响车铃铛 (快捷键 B)">
🔔 叮叮!
</button>
</div>
<!-- Speed Slider -->
<div class="control-group">
<div class="speed-slider-wrap">
<span class="speed-label" id="speedText">速度: 正常</span>
<input type="range" id="speedSlider" min="0.3" max="2.6" step="0.1" value="1.0" />
</div>
</div>
<!-- Actions: Sprint & Time of Day -->
<div class="control-group">
<button class="btn" id="turboBtn" title="冲刺狂飙模式">
⚡ 冲刺
</button>
<button class="btn" id="themeBtn" title="切换早晨/黄昏/夜景">
🌅 场景
</button>
</div>
</div>
</div> <!-- End Main Wrapper -->
<script>
/* ==========================================================
1. WEB AUDIO SYNTHESIZER (Pure JS Zero-Dependency SFX)
========================================================== */
class SoundEngine {
constructor() {
this.ctx = null;
this.enabled = true;
}
init() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
// Crisp double bicycle bell "Ding-Ding!"
ringBell() {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
const now = this.ctx.currentTime;
const playChime = (time, freq) => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, time);
// Bell envelope
gain.gain.setValueAtTime(0.35, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.55);
// Subtle overtone
const osc2 = this.ctx.createOscillator();
const gain2 = this.ctx.createGain();
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(freq * 2.76, time);
gain2.gain.setValueAtTime(0.12, time);
gain2.gain.exponentialRampToValueAtTime(0.001, time + 0.3);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc2.connect(gain2);
gain2.connect(this.ctx.destination);
osc.start(time);
osc2.start(time);
osc.stop(time + 0.6);
osc2.stop(time + 0.35);
};
playChime(now, 2093); // First Ding (C7)
playChime(now + 0.12, 2637); // Second Ding (E7)
}
// Play cheerful pelican chirp / chuckle
pelicanSquawk() {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(320, now);
osc.frequency.exponentialRampToValueAtTime(580, now + 0.08);
osc.frequency.exponentialRampToValueAtTime(240, now + 0.22);
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0.2, now + 0.06);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
// Low-pass filter for friendly rounded tone
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1400, now);
osc.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.28);
}
// Toggle button feedback click
clickTone() {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.frequency.setValueAtTime(600, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(200, this.ctx.currentTime + 0.05);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.05);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.06);
}
}
const sound = new SoundEngine();
/* ==========================================================
2. SVG ELEMENT REFERENCES & DYNAMIC GENERATION
========================================================== */
const stage = document.getElementById('stage');
const container = document.getElementById('canvasContainer');
// Wheels & Spokes
const spokesRear = document.getElementById('spokesRear');
const spokesFront = document.getElementById('spokesFront');
const numSpokes = 16;
for (let i = 0; i < numSpokes; i++) {
const angle = (i * 360) / numSpokes;
const rad = (angle * Math.PI) / 180;
const x2 = Math.cos(rad) * 64;
const y2 = Math.sin(rad) * 64;
const spokeR = document.createElementNS('http://www.w3.org/2000/svg', 'line');
spokeR.setAttribute('x1', '0');
spokeR.setAttribute('y1', '0');
spokeR.setAttribute('x2', x2.toFixed(1));
spokeR.setAttribute('y2', y2.toFixed(1));
spokesRear.appendChild(spokeR);
const spokeF = document.createElementNS('http://www.w3.org/2000/svg', 'line');
spokeF.setAttribute('x1', '0');
spokeF.setAttribute('y1', '0');
spokeF.setAttribute('x2', x2.toFixed(1));
spokeF.setAttribute('y2', y2.toFixed(1));
spokesFront.appendChild(spokeF);
}
// Fence Posts Generation (Parallax promenade railing)
const fencePostsGroup = document.getElementById('fencePosts');
const fenceSpacing = 85;
const numFencePosts = Math.ceil(1300 / fenceSpacing) + 2;
const fenceElements = [];
for (let i = 0; i < numFencePosts; i++) {
const post = document.createElementNS('http://www.w3.org/2000/svg', 'line');
post.setAttribute('y1', '448');
post.setAttribute('y2', '465');
post.setAttribute('stroke', '#64748b');
post.setAttribute('stroke-width', '4');
post.setAttribute('stroke-linecap', 'round');
fencePostsGroup.appendChild(post);
fenceElements.push(post);
}
// Roadside Foliage Generation
const foliageGroup = document.getElementById('roadsideFoliage');
const foliageItems = [];
for (let i = 0; i < 14; i++) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
// Grass tuft
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
p.setAttribute('d', 'M0 0 L-6 -14 L0 -6 L6 -16 L4 0 Z');
p.setAttribute('fill', i % 2 === 0 ? '#10b981' : '#059669');
g.appendChild(p);
// Cute roadside flower occasionally
if (i % 3 === 0) {
const f = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
f.setAttribute('cx', '6');
f.setAttribute('cy', '-16');
f.setAttribute('r', '3');
f.setAttribute('fill', i % 6 === 0 ? '#f43f5e' : '#fbbf24');
g.appendChild(f);
}
g.x = i * 95 + Math.random() * 40;
g.y = 482;
foliageGroup.appendChild(g);
foliageItems.push(g);
}
// Interactive Ding visual wave
function spawnDingEffect(x, y, text = '叮~ 🔔') {
const el = document.createElement('div');
el.className = 'ding-effect';
el.innerText = text;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
container.appendChild(el);
setTimeout(() => el.remove(), 1000);
}
/* ==========================================================
3. 2D INVERSE KINEMATICS (IK) FOR PELICAN PEDALING LEGS
========================================================== */
/**
* Solves 2-bone planar IK:
* Hip (x1, y1) -> Knee (kx, ky) -> Pedal (x2, y2)
* L1: Thigh length, L2: Shin length
* flipKnee: determines bend direction
*/
function solveIK(x1, y1, x2, y2, L1, L2, flipKnee = false) {
const dx = x2 - x1;
const dy = y2 - y1;
let dist = Math.hypot(dx, dy);
// Clamp distance to avoid triangle inequality breakdown
const maxDist = L1 + L2 - 0.5;
const minDist = Math.abs(L1 - L2) + 0.5;
if (dist > maxDist) dist = maxDist;
if (dist < minDist) dist = minDist;
const alpha = Math.atan2(dy, dx);
// Cosine rule
const cosBeta = (L1 * L1 + dist * dist - L2 * L2) / (2 * L1 * dist);
const beta = Math.acos(Math.max(-1, Math.min(1, cosBeta)));
const kneeAngle = flipKnee ? (alpha - beta) : (alpha + beta);
const kx = x1 + Math.cos(kneeAngle) * L1;
const ky = y1 + Math.sin(kneeAngle) * L1;
return { kx, ky };
}
/* ==========================================================
4. ANIMATION CONTROLLER & STATE
========================================================== */
const state = {
isPlaying: true,
speedMultiplier: 1.0,
turboMode: false,
currentTheme: 'day', // 'day' | 'sunset' | 'night'
// Coordinates & Rig parameters
time: 0,
pedalAngle: 0,
spokeRotation: 0,
// Bottom bracket (crank center)
bbX: 550,
bbY: 485,
crankRadius: 32,
// Hip joint anchor
hipBaseX: 495,
hipBaseY: 325,
// Leg segment lengths
thighLen: 82,
shinLen: 84,
// Parallax offsets
roadOffset: 0,
fenceOffset: 0,
cloudsOffset: 0,
wavesOffset: 0,
// Bobbing momentum
bodyBobY: 0,
headBobAngle: 0,
pouchJiggle: 0,
scarfWave: 0
};
// DOM Elements for Animation Updates
const crankArmRight = document.getElementById('crankArmRight');
const crankArmLeft = document.getElementById('crankArmLeft');
const pedalRight = document.getElementById('pedalRight');
const pedalLeft = document.getElementById('pedalLeft');
const pelicanLegRight = document.getElementById('pelicanLegRight');
const pelicanLegLeft = document.getElementById('pelicanLegLeft');
const pelicanFootRight = document.getElementById('pelicanFootRight');
const pelicanFootLeft = document.getElementById('pelicanFootLeft');
const kneeRightCircle = document.getElementById('kneeRight');
const actorRoot = document.getElementById('actorRoot');
const pelicanHead = document.getElementById('pelicanHead');
const throatPouch = document.getElementById('throatPouch');
const eyelid = document.getElementById('eyelid');
const pupil = document.getElementById('pupil');
const scarfTailBack = document.getElementById('scarfTailBack');
const scarfTailFront = document.getElementById('scarfTailFront');
const pelicanTail = document.getElementById('pelicanTail');
const roadLines = document.getElementById('roadLines');
const waveLines = document.getElementById('waveLines');
const cloudLayer = document.getElementById('cloudLayer');
const speedLinesBack = document.getElementById('speedLinesBack');
const speedLinesFront = document.getElementById('speedLinesFront');
const dustParticlesGroup = document.getElementById('dustParticles');
const lightBeam = document.getElementById('lightBeam');
// Blink timer
let blinkTimer = 0;
let isBlinking = false;
// Dust particles array
const dusts = [];
function spawnDust(x, y, vx, vy) {
if (dusts.length > 25) return;
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', x);
c.setAttribute('cy', y);
c.setAttribute('r', (Math.random() * 4 + 2).toFixed(1));
c.setAttribute('fill', state.currentTheme === 'night' ? 'rgba(100,116,139,0.7)' : 'rgba(210,210,200,0.8)');
dustParticlesGroup.appendChild(c);
dusts.push({ el: c, x, y, vx, vy, life: 1.0, decay: Math.random() * 0.03 + 0.02 });
}
/* ==========================================================
5. MAIN ANIMATION LOOP (requestAnimationFrame)
========================================================== */
let lastTimestamp = 0;
function animate(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const deltaMs = Math.min(timestamp - lastTimestamp, 50);
lastTimestamp = timestamp;
if (state.isPlaying) {
const effectiveSpeed = state.speedMultiplier * (state.turboMode ? 2.1 : 1.0);
const dt = (deltaMs / 1000) * effectiveSpeed;
state.time += dt;
// 1. Angular velocities
const cadenceRad = 4.2; // Radians per second at 1.0x speed
state.pedalAngle += cadenceRad * dt;
state.spokeRotation = (state.spokeRotation + 360 * dt * 1.6) % 360;
// Rotate wheel spokes
spokesRear.setAttribute('transform', `rotate(${state.spokeRotation.toFixed(1)})`);
spokesFront.setAttribute('transform', `rotate(${state.spokeRotation.toFixed(1)})`);
// 2. Parallax backgrounds
// Road dashed line
state.roadOffset = (state.roadOffset - 420 * dt) % 100;
roadLines.setAttribute('stroke-dashoffset', state.roadOffset.toFixed(1));
// Fence posts shifting
state.fenceOffset = (state.fenceOffset - 280 * dt) % fenceSpacing;
for (let i = 0; i < fenceElements.length; i++) {
const posX = i * fenceSpacing + state.fenceOffset;
fenceElements[i].setAttribute('x1', posX.toFixed(1));
fenceElements[i].setAttribute('x2', posX.toFixed(1));
}
// Roadside foliage shifting
foliageItems.forEach(item => {
item.x -= 420 * dt;
if (item.x < -30) item.x = 1230 + Math.random() * 40;
item.setAttribute('transform', `translate(${item.x.toFixed(1)}, ${item.y})`);
});
// Ocean waves shifting
state.wavesOffset = (state.wavesOffset - 75 * dt) % 120;
waveLines.setAttribute('transform', `translate(${state.wavesOffset.toFixed(1)}, 0)`);
// Clouds drifting
state.cloudsOffset = (state.cloudsOffset - 18 * dt) % 600;
cloudLayer.setAttribute('transform', `translate(${state.cloudsOffset.toFixed(1)}, 0)`);
// 3. Bicycle & Pelican Natural Bobbing & Dynamics
// Natural dual-pulse bobbing per pedal revolution
const bobAmount = state.turboMode ? 4.5 : 2.5;
state.bodyBobY = Math.sin(state.pedalAngle * 2) * bobAmount;
const forwardLean = state.turboMode ? 2.5 : 0;
actorRoot.setAttribute('transform', `translate(0, ${state.bodyBobY.toFixed(2)}) rotate(${forwardLean}, 550, 485)`);
// Head bounce with phase lag
state.headBobAngle = Math.sin(state.pedalAngle * 2 - 0.7) * (state.turboMode ? 5.5 : 3.0);
pelicanHead.setAttribute('transform', `translate(640, 160) rotate(${state.headBobAngle.toFixed(2)})`);
// Throat pouch elastic jiggle
state.pouchJiggle = Math.sin(state.pedalAngle * 2 - 1.2) * 4;
const pouchY = (85 + state.pouchJiggle).toFixed(1);
const pouchX = (20 + state.pouchJiggle * 0.4).toFixed(1);
throatPouch.setAttribute('d', `M12 8 C25 25 35 70 ${pouchX} ${pouchY} C5 98 -25 90 -30 65 C-35 45 -25 22 -15 12 Z`);
// Tail feathers gentle wiggle
const tailWobble = Math.sin(state.pedalAngle) * 3;
pelicanTail.setAttribute('transform', `translate(425, 310) rotate(${tailWobble.toFixed(1)})`);
// Scarf tails waving dynamically in wind
const scarfPhase = state.time * (state.turboMode ? 22 : 12);
const s1 = Math.sin(scarfPhase) * 14;
const s2 = Math.cos(scarfPhase * 1.3) * 16;
scarfTailBack.setAttribute('d', `M570 220 Q510 ${200 + s1} 460 ${215 - s2} T400 ${210 + s1}`);
scarfTailFront.setAttribute('d', `M575 224 Q520 ${212 - s2} 475 ${230 + s1} T415 ${228 - s2}`);
// 4. Right & Left Pedals & Legs Kinematics (IK)
// Right pedal (Near side, angle: pedalAngle)
const pR_x = state.bbX + Math.cos(state.pedalAngle) * state.crankRadius;
const pR_y = state.bbY + Math.sin(state.pedalAngle) * state.crankRadius;
// Left pedal (Far side, 180 degrees out of phase)
const pL_x = state.bbX + Math.cos(state.pedalAngle + Math.PI) * state.crankRadius;
const pL_y = state.bbY + Math.sin(state.pedalAngle + Math.PI) * state.crankRadius;
// Update Right Crank Arm & Pedal
crankArmRight.setAttribute('x2', pR_x.toFixed(1));
crankArmRight.setAttribute('y2', pR_y.toFixed(1));
pedalRight.setAttribute('x', (pR_x - 12).toFixed(1));
pedalRight.setAttribute('y', (pR_y - 4).toFixed(1));
// Update Left Crank Arm & Pedal
crankArmLeft.setAttribute('x2', pL_x.toFixed(1));
crankArmLeft.setAttribute('y2', pL_y.toFixed(1));
pedalLeft.setAttribute('x', (pL_x - 11).toFixed(1));
pedalLeft.setAttribute('y', (pL_y - 4).toFixed(1));
// Solve IK for Right Leg
const hipX = state.hipBaseX;
const hipY = state.hipBaseY;
const ikRight = solveIK(hipX, hipY, pR_x, pR_y, state.thighLen, state.shinLen, false);
pelicanLegRight.setAttribute('d', `M${hipX} ${hipY} L${ikRight.kx.toFixed(1)} ${ikRight.ky.toFixed(1)} L${pR_x.toFixed(1)} ${pR_y.toFixed(1)}`);
kneeRightCircle.setAttribute('cx', ikRight.kx.toFixed(1));
kneeRightCircle.setAttribute('cy', ikRight.ky.toFixed(1));
// Foot angle aligns with pedal with slight natural flex
const footAngle = Math.sin(state.pedalAngle) * 0.25;
const fR_tipX = pR_x + Math.cos(footAngle) * 26;
const fR_tipY = pR_y + Math.sin(footAngle) * 8;
const fR_heelX = pR_x - 12;
const fR_heelY = pR_y + 6;
pelicanFootRight.setAttribute('points', `${pR_x.toFixed(1)},${pR_y.toFixed(1)} ${fR_tipX.toFixed(1)},${fR_tipY.toFixed(1)} ${fR_heelX.toFixed(1)},${fR_heelY.toFixed(1)}`);
// Solve IK for Left Leg (Far side)
const ikLeft = solveIK(hipX - 5, hipY - 2, pL_x, pL_y, state.thighLen, state.shinLen, false);
pelicanLegLeft.setAttribute('d', `M${hipX - 5} ${hipY - 2} L${ikLeft.kx.toFixed(1)} ${ikLeft.ky.toFixed(1)} L${pL_x.toFixed(1)} ${pL_y.toFixed(1)}`);
const fL_tipX = pL_x + Math.cos(footAngle + 0.3) * 24;
const fL_tipY = pL_y + Math.sin(footAngle + 0.3) * 8;
const fL_heelX = pL_x - 10;
const fL_heelY = pL_y + 6;
pelicanFootLeft.setAttribute('points', `${pL_x.toFixed(1)},${pL_y.toFixed(1)} ${fL_tipX.toFixed(1)},${fL_tipY.toFixed(1)} ${fL_heelX.toFixed(1)},${fL_heelY.toFixed(1)}`);
// 5. Eye Blinking Logic
blinkTimer += dt;
if (!isBlinking && blinkTimer > 2.8 + Math.random() * 2) {
isBlinking = true;
blinkTimer = 0;
eyelid.setAttribute('d', 'M-9 -11 Q0 12 9 -11 L9 11 Q0 12 -9 11 Z');
setTimeout(() => {
eyelid.setAttribute('d', 'M-9 -11 Q0 -12 9 -11 L9 -11 Q0 -11 -9 -11 Z');
isBlinking = false;
}, 140);
}
// 6. Dust particle emissions at rear tire
if (Math.random() < (state.turboMode ? 0.65 : 0.25)) {
spawnDust(
410 + (Math.random() - 0.5) * 8,
556 + Math.random() * 4,
-180 - Math.random() * 80,
-15 - Math.random() * 20
);
}
// Update & cull dust particles
for (let i = dusts.length - 1; i >= 0; i--) {
const d = dusts[i];
d.x += d.vx * dt;
d.y += d.vy * dt;
d.life -= d.decay;
if (d.life <= 0) {
d.el.remove();
dusts.splice(i, 1);
} else {
d.el.setAttribute('cx', d.x.toFixed(1));
d.el.setAttribute('cy', d.y.toFixed(1));
d.el.setAttribute('opacity', d.life.toFixed(2));
}
}
// 7. Turbo Speed Streaks
if (state.turboMode) {
if (Math.random() < 0.4) {
const yPos = 180 + Math.random() * 320;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', '1220');
line.setAttribute('y1', yPos);
line.setAttribute('x2', `${1220 + 80 + Math.random() * 120}`);
line.setAttribute('y2', yPos);
line.setAttribute('stroke', 'rgba(255,255,255,0.7)');
line.setAttribute('stroke-width', (Math.random() * 2 + 1.5).toFixed(1));
speedLinesFront.appendChild(line);
const speed = 1400 + Math.random() * 400;
const streakObj = { el: line, x: 1220, y: yPos, speed };
const streakTimer = setInterval(() => {
streakObj.x -= speed * 0.016;
line.setAttribute('x1', streakObj.x);
line.setAttribute('x2', streakObj.x + 120);
if (streakObj.x < -200) {
clearInterval(streakTimer);
line.remove();
}
}, 16);
}
}
}
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
/* ==========================================================
6. USER INTERACTION & CONTROLS
========================================================== */
const playPauseBtn = document.getElementById('playPauseBtn');
const playIcon = document.getElementById('playIcon');
const ringBellBtn = document.getElementById('ringBellBtn');
const speedSlider = document.getElementById('speedSlider');
const speedText = document.getElementById('speedText');
const turboBtn = document.getElementById('turboBtn');
const themeBtn = document.getElementById('themeBtn');
const soundBtn = document.getElementById('soundBtn');
const soundIcon = document.getElementById('soundIcon');
const soundText = document.getElementById('soundText');
const bikeBellSvg = document.getElementById('bikeBell');
const throatPouchGroup = document.getElementById('throatPouchGroup');
// Play / Pause
playPauseBtn.addEventListener('click', () => {
sound.clickTone();
state.isPlaying = !state.isPlaying;
playIcon.textContent = state.isPlaying ? '⏸️' : '▶️';
playPauseBtn.classList.toggle('active', !state.isPlaying);
});
// Speed Slider
speedSlider.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
state.speedMultiplier = val;
if (val <= 0.6) {
speedText.textContent = '速度: 悠闲';
} else if (val <= 1.2) {
speedText.textContent = '速度: 正常';
} else if (val <= 1.8) {
speedText.textContent = '速度: 疾速';
} else {
speedText.textContent = '速度: 飞驰';
}
});
// Ring Bell Handler
function triggerBellRing(e) {
sound.ringBell();
// Calculate coordinates for visual ding bubble
let clientX = window.innerWidth / 2;
let clientY = window.innerHeight / 2 - 40;
if (e && e.clientX) {
clientX = e.clientX;
clientY = e.clientY - 20;
} else {
const rect = bikeBellSvg.getBoundingClientRect();
clientX = rect.left + rect.width / 2;
clientY = rect.top;
}
spawnDingEffect(clientX, clientY, '叮叮~ 🎵');
// Quick chime animation on bell
bikeBellSvg.style.transform = 'translate(670px, 255px) scale(1.3) rotate(-15deg)';
setTimeout(() => {
bikeBellSvg.style.transform = 'translate(670px, 255px) scale(1) rotate(0deg)';
}, 180);
}
ringBellBtn.addEventListener('click', triggerBellRing);
bikeBellSvg.addEventListener('click', triggerBellRing);
// Turbo Mode Toggle
turboBtn.addEventListener('click', () => {
sound.clickTone();
state.turboMode = !state.turboMode;
turboBtn.classList.toggle('active', state.turboMode);
if (state.turboMode) {
turboBtn.textContent = '🔥 狂飙中!';
sound.pelicanSquawk();
} else {
turboBtn.textContent = '⚡ 冲刺';
}
});
// Theme Switcher (Day -> Sunset -> Night)
const themes = ['day', 'sunset', 'night'];
const themeLabels = { day: '🌅 黄昏', sunset: '🌙 夜晚', night: '☀️ 白天' };
themeBtn.addEventListener('click', () => {
sound.clickTone();
const nextIndex = (themes.indexOf(state.currentTheme) + 1) % themes.length;
state.currentTheme = themes[nextIndex];
document.body.setAttribute('data-theme', state.currentTheme);
themeBtn.textContent = themeLabels[state.currentTheme];
// Night mode headlight control
if (state.currentTheme === 'night') {
lightBeam.setAttribute('opacity', '1');
} else {
lightBeam.setAttribute('opacity', '0');
}
});
// Sound On/Off Toggle
soundBtn.addEventListener('click', () => {
sound.enabled = !sound.enabled;
soundIcon.textContent = sound.enabled ? '🔊' : '🔇';
soundText.textContent = sound.enabled ? '音效开' : '静音中';
if (sound.enabled) sound.clickTone();
});
// Click on pelican throat pouch: fish giggles and pelican squawks!
throatPouchGroup.style.cursor = 'pointer';
throatPouchGroup.addEventListener('click', (e) => {
sound.pelicanSquawk();
const rect = throatPouchGroup.getBoundingClientRect();
spawnDingEffect(rect.left + rect.width / 2, rect.top, '嘎嘎! 🐟');
});
// Keyboard Shortcuts
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyB') {
triggerBellRing();
} else if (e.code === 'Space') {
e.preventDefault();
playPauseBtn.click();
} else if (e.code === 'KeyT') {
turboBtn.click();
}
});
// First user gesture initialization for AudioContext
window.addEventListener('pointerdown', () => sound.init(), { once: true });
</script>
</body>
</html>
最新回复 (0)
没有回复
* 帖子来源NodeSeek
附近帖子
- ↑DMIT 溢价这么高 现在出了换其他JJ是不是很香
- ↑e卡陨落了😭返现一坨,u卡该何去何从
- ↑不知道为什么会设置头像失败啊😕
- ↑天翼云
- ↑nosla的圣何塞优化,和vmiss tri,不谈价格,哪个体验更好啊
- 📍 鹈鹕骑自行车 Antigravity IDE Gemini 3.8 Flash High 这么强的吗?
- ↓grok 4.6 鹈鹕测试
- ↓海贼王漫画从哪下载
- ↓看到移动的香港这么香和便宜,有没有mjj在家里家装一条移动线路,一条电信呢
- ↓大佬们都买的什么小鸡啊
- ↓dmit大概什么时候补货啊,有消息吗?