最终回答
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pelican Cyclist</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Fredoka', sans-serif;
background-color: #f0f9ff;
overflow: hidden;
}
/* Glassmorphism for controls */
.glass-panel {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
/* Slider Styling */
input[type=range] {
-webkit-appearance: none;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: #f97316;
cursor: pointer;
margin-top: -8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #cbd5e1;
border-radius: 2px;
}
.select-none {
user-select: none;
}
</style>
</head>
<body class="h-screen w-screen flex flex-col items-center justify-center relative">
<!-- Main Scene Container -->
<div class="relative w-full max-w-4xl aspect-video bg-white rounded-3xl shadow-2xl overflow-hidden border-4 border-white ring-1 ring-slate-200">
<!-- Sky Gradient Background -->
<div id="sky-bg" class="absolute inset-0 bg-gradient-to-b from-sky-300 via-sky-100 to-white transition-colors duration-700"></div>
<!-- SVG Canvas -->
<svg id="scene" viewBox="0 0 800 450" class="w-full h-full relative z-10" preserveAspectRatio="xMidYMid slice">
<defs>
<!-- Bike Frame Gradient -->
<linearGradient id="frameGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ec4899;stop-opacity:1" />
<stop offset="100%" style="stop-color:#be185d;stop-opacity:1" />
</linearGradient>
<!-- Metal Gradient -->
<linearGradient id="metalGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#94a3b8;stop-opacity:1" />
<stop offset="50%" style="stop-color:#cbd5e1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#64748b;stop-opacity:1" />
</linearGradient>
<!-- Beak Gradient -->
<linearGradient id="beakGrad" x1="0%" y1="0%" x2="100%" y2="50%">
<stop offset="0%" style="stop-color:#fb923c;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f97316;stop-opacity:1" />
</linearGradient>
<!-- Wheel Spoke Pattern -->
<pattern id="spokes" width="20" height="20" patternUnits="userSpaceOnUse">
<line x1="10" y1="0" x2="10" y2="20" stroke="#cbd5e1" stroke-width="1" />
<line x1="0" y1="10" x2="20" y2="10" stroke="#cbd5e1" stroke-width="1" />
</pattern>
</defs>
<!-- Dynamic Background Elements Group -->
<g id="bg-elements">
<!-- Clouds will be injected here via JS -->
<!-- Road Lines -->
<rect x="-100" y="380" width="1200" height="10" fill="#e2e8f0" />
</g>
<!-- Bike Group -->
<g id="bike-assembly" transform="translate(0, 10)">
<!-- Back Wheel -->
<g id="wheel-back" transform="translate(250, 350)">
<circle r="70" fill="none" stroke="#334155" stroke-width="8" /> <!-- Tire -->
<circle r="62" fill="none" stroke="#cbd5e1" stroke-width="2" /> <!-- Rim -->
<g id="spokes-back">
<line x1="0" y1="-62" x2="0" y2="62" stroke="#cbd5e1" stroke-width="1" />
<line x1="-53" y1="-31" x2="53" y2="31" stroke="#cbd5e1" stroke-width="1" />
<line x1="-53" y1="31" x2="53" y2="-31" stroke="#cbd5e1" stroke-width="1" />
</g>
<circle r="5" fill="#475569" /> <!-- Hub -->
</g>
<!-- Front Wheel -->
<g id="wheel-front" transform="translate(550, 350)">
<circle r="70" fill="none" stroke="#334155" stroke-width="8" />
<circle r="62" fill="none" stroke="#cbd5e1" stroke-width="2" />
<g id="spokes-front">
<line x1="0" y1="-62" x2="0" y2="62" stroke="#cbd5e1" stroke-width="1" />
<line x1="-53" y1="-31" x2="53" y2="31" stroke="#cbd5e1" stroke-width="1" />
<line x1="-53" y1="31" x2="53" y2="-31" stroke="#cbd5e1" stroke-width="1" />
</g>
<circle r="5" fill="#475569" />
</g>
<!-- Frame -->
<!-- Main triangle -->
<path d="M250,350 L350,180 L520,200 L400,350 Z" fill="none" stroke="url(#frameGrad)" stroke-width="12" stroke-linejoin="round" stroke-linecap="round" />
<!-- Fork -->
<path d="M550,350 L520,200" fill="none" stroke="url(#frameGrad)" stroke-width="10" stroke-linecap="round" />
<!-- Seat post -->
<path d="M350,180 L340,140" fill="none" stroke="url(#frameGrad)" stroke-width="10" stroke-linecap="round" />
<!-- Handlebar Stem -->
<path d="M520,200 L530,150" fill="none" stroke="#475569" stroke-width="8" stroke-linecap="round" />
<!-- Handlebars -->
<path d="M510,150 L560,150 L570,130" fill="none" stroke="#cbd5e1" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
<!-- Seat -->
<path d="M320,140 Q380,130 380,150 L320,150 Z" fill="#334155" />
<!-- Crank & Pedals -->
<g id="crank-assembly" transform="translate(400, 350)">
<!-- Left Pedal (Behind) -->
<g id="pedal-left-group">
<line x1="0" y1="0" x2="0" y2="30" stroke="#334155" stroke-width="6" />
<rect x="-10" y="25" width="20" height="6" fill="#1e293b" rx="2" />
</g>
<!-- Chainring -->
<circle r="15" fill="none" stroke="#64748b" stroke-width="4" />
<!-- Right Pedal (Front) -->
<g id="pedal-right-group">
<line x1="0" y1="0" x2="0" y2="-30" stroke="#334155" stroke-width="6" />
<rect x="-10" y="-31" width="20" height="6" fill="#1e293b" rx="2" />
</g>
</g>
</g>
<!-- Pelican Group -->
<g id="pelican" transform="translate(340, 140)">
<!-- Far Leg -->
<path id="leg-far" d="M20,40 L20,80 L-10,90" fill="none" stroke="#f97316" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
<path id="foot-far" d="M-10,90 L-20,95" stroke="#f97316" stroke-width="5" stroke-linecap="round" />
<!-- Tail -->
<path d="M-10,10 L-40,30 L-10,40 Z" fill="#ffffff" stroke="#e2e8f0" stroke-width="1" />
<!-- Body -->
<ellipse cx="20" cy="30" rx="45" ry="35" fill="#ffffff" />
<!-- Wings -->
<path d="M-10,10 Q20,50 60,20 L60,0 Q30,-10 -10,10 Z" fill="#f1f5f9" stroke="#e2e8f0" stroke-width="1"/>
<!-- Neck & Head -->
<!-- Neck is a path that we might animate slightly for balance -->
<path id="neck" d="M50,20 Q60,50 60,80 L60,90" fill="none" stroke="#ffffff" stroke-width="18" stroke-linecap="round" />
<g id="head-group" transform="translate(60, 90)">
<!-- Head Circle -->
<circle cx="0" cy="0" r="18" fill="#ffffff" />
<!-- Eye -->
<circle cx="5" cy="-5" r="2.5" fill="#0f172a" />
<circle cx="6" cy="-6" r="1" fill="white" />
<!-- Lower Beak -->
<path d="M10,2 Q70,2 100,10 L10,12 Z" fill="#fbbf24" stroke="#f59e0b" stroke-width="1" />
<!-- Pouch -->
<path id="pouch" d="M15,12 Q50,40 85,10 Q80,5 60,10" fill="#fca5a5" stroke="#f87171" stroke-width="1" />
<!-- Upper Beak -->
<path d="M10,-5 Q75,-5 105,5 L10,2 Z" fill="url(#beakGrad)" stroke="#ea580c" stroke-width="1" />
<!-- Cap/Hat (The Cyclist) -->
<path d="M-18,-2 Q0,-25 18,-2 Z" fill="#ef4444" /> <!-- Cap dome -->
<path d="M-5,-15 L5,-15 L0,-25 Z" fill="white" /> <!-- Logo detail -->
<path d="M-18,-2 L25,5 L18,-2 Z" fill="#ef4444" /> <!-- Bill -->
</g>
<!-- Near Leg -->
<path id="leg-near" d="M30,40 L30,80 L0,90" fill="none" stroke="#f97316" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" />
<path id="foot-near" d="M0,90 L-10,95" stroke="#f97316" stroke-width="5" stroke-linecap="round" />
</g>
<g id="handlebar-streamers">
<!-- Streamers coming off handlebars -->
<path id="streamer1" d="M560,150 Q580,140 590,160 T610,150" fill="none" stroke="#3b82f6" stroke-width="3" opacity="0.8" />
<path id="streamer2" d="M560,150 Q580,160 590,140 T610,160" fill="none" stroke="#f43f5e" stroke-width="3" opacity="0.8" />
</g>
</svg>
<!-- Overlay Text/Greeting -->
<div class="absolute bottom-24 left-8">
<div class="bg-white/80 px-4 py-2 rounded-lg shadow-lg transform -rotate-2 border border-orange-100">
<p class="text-slate-700 font-bold text-lg">Morning Ride 🚲</p>
<p class="text-slate-500 text-sm">Pedal Power!</p>
</div>
</div>
</div>
<!-- Controls Panel -->
<div class="mt-6 glass-panel rounded-2xl p-6 flex flex-col md:flex-row items-center gap-8 z-20">
<!-- Speed Control -->
<div class="flex flex-col w-48">
<div class="flex justify-between mb-2">
<label class="text-slate-700 font-semibold text-sm uppercase tracking-wider">Speed</label>
<span id="speed-display" class="text-orange-500 font-bold">0 km/h</span>
</div>
<input type="range" id="speed-slider" min="0" max="50" value="15" class="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="w-px h-12 bg-slate-300 hidden md:block"></div>
<!-- Theme Control -->
<div class="flex items-center gap-4">
<span class="text-slate-700 font-semibold text-sm uppercase tracking-wider">Theme</span>
<button id="theme-toggle" class="relative inline-flex h-8 w-14 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-600 focus-visible:ring-offset-2 bg-sky-200">
<span class="sr-only">Use setting</span>
<span id="toggle-circle" aria-hidden="true" class="pointer-events-none inline-block h-7 w-7 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out translate-x-0 flex items-center justify-center text-xs">☀️</span>
</button>
</div>
</div>
<!-- JavaScript Logic -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- Configuration ---
const CONFIG = {
maxSpeed: 50,
crankRadius: 30, // Distance from center to pedal
legLengthThigh: 50,
legLengthShin: 50,
hipOffsetX: 20, // Relative to pelican group
hipOffsetY: 30
};
// --- Elements ---
const els = {
wheelBack: document.getElementById('wheel-back'),
wheelFront: document.getElementById('wheel-front'),
pedalLeftGroup: document.getElementById('pedal-left-group'),
pedalRightGroup: document.getElementById('pedal-right-group'),
legNear: document.getElementById('leg-near'), // Thigh (Hip to Knee), Shin (Knee to Foot)
footNear: document.getElementById('foot-near'), // Foot
legFar: document.getElementById('leg-far'),
footFar: document.getElementById('foot-far'),
pelican: document.getElementById('pelican'),
pouch: document.getElementById('pouch'),
neck: document.getElementById('neck'),
streamer1: document.getElementById('streamer1'),
streamer2: document.getElementById('streamer2'),
themeToggle: document.getElementById('theme-toggle'),
toggleCircle: document.getElementById('toggle-circle'),
skyBg: document.getElementById('sky-bg'),
speedSlider: document.getElementById('speed-slider'),
speedDisplay: document.getElementById('speed-display'),
bgElements: document.getElementById('bg-elements')
};
// --- State ---
let state = {
speed: 15, // 0 to 50
distance: 0,
time: 0,
isNight: false,
clouds: []
};
// Initialize Clouds
for(let i=0; i<4; i++) {
const cloud = createCloud(i);
state.clouds.push(cloud);
els.bgElements.insertBefore(cloud.el, els.bgElements.firstChild);
}
function createCloud(index) {
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
const y = 20 + Math.random() * 100;
const scale = 0.5 + Math.random() * 1;
const x = Math.random() * 800;
// Simple cloud shape
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M25,60 Q40,30 65,50 Q80,20 110,45 Q135,25 150,55 Q170,50 160,80 L40,80 Q20,80 25,60 Z");
path.setAttribute("fill", "white");
path.setAttribute("opacity", "0.8");
g.appendChild(path);
g.setAttribute("transform", `translate(${x}, ${y}) scale(${scale})`);
return { el: g, x: x, y: y, scale: scale, speed: 0.2 + Math.random() * 0.3 };
}
// --- Inverse Kinematics (IK) for Legs ---
// Solves the knee position given Hip (fixed), Foot (moving on circle), and lengths
function solveIK(hipX, hipY, footX, footY, lenThigh, lenShin) {
const dist = Math.sqrt((footX - hipX)**2 + (footY - hipY)**2);
// If unreachable, straighten leg
if (dist > lenThigh + lenShin) {
const ratio = lenThigh / (lenThigh + lenShin);
return {
x: hipX + (footX - hipX) * ratio,
y: hipY + (footY - hipY) * ratio
};
}
// Law of Cosines
// a = shin, b = thigh, c = dist
// angle at hip
const alpha = Math.acos( (lenThigh**2 + dist**2 - lenShin**2) / (2 * lenThigh * dist) );
const angleToFoot = Math.atan2(footY - hipY, footX - hipX);
// Two solutions (knee forward or back). We want knee forward (negative relative angle for right leg typically)
// Adjust based on expected anatomy.
const kneeAngle = angleToFoot + alpha;
return {
x: hipX + lenThigh * Math.cos(kneeAngle),
y: hipY + lenThigh * Math.sin(kneeAngle)
};
}
// --- Animation Loop ---
function animate() {
// Update Time & Distance
state.time += 0.01 * (state.speed / 10);
state.distance += state.speed;
// 1. Rotate Wheels & Crank
const rotation = -(state.distance * 2) % 360;
const crankRot = -(state.distance * 2) % 360;
els.wheelBack.setAttribute("transform", `translate(250, 350) rotate(${rotation})`);
els.wheelFront.setAttribute("transform", `translate(550, 350) rotate(${rotation})`);
// Animate Pedals
// Pedal Right (Front) relative to crank center (0,0)
const rad = crankRot * Math.PI / 180;
const px = Math.sin(rad) * CONFIG.crankRadius;
const py = Math.cos(rad) * CONFIG.crankRadius;
els.pedalRightGroup.setAttribute("transform", `translate(${px}, ${py})`);
els.pedalLeftGroup.setAttribute("transform", `translate(${-px}, ${-py})`);
// 2. Animate Legs (IK)
// Pelican Hip Global position roughly:
// Bike Center (400, 350). Pelican Group Translate (340, 140).
// Hip offset inside pelican (30, 40).
// Global Hip: 340 + 30 = 370 (X), 140 + 40 = 180 (Y).
// But we are calculating relative to the bike group's coordinate space?
// The legs are in the pelican group. The pedals are in the crank-assembly group.
// Pelican Group Trans: 340, 140.
// Crank Center Trans: 400, 350.
// Foot Position (Crank relative): 400 + px, 350 + py.
// Hip Position (Global): 340 + 30 = 370, 140 + 40 = 180.
// Hip Position (Relative to Crank Center): 370 - 400 = -30, 180 - 350 = -170.
const hipX = -30;
const hipY = -170;
// Near Leg (Right Leg) Logic
const kneeNear = solveIK(hipX, hipY, px, py, CONFIG.legLengthThigh, CONFIG.legLengthShin);
// Construct Path: M Hip, L Knee, L Foot
// Global coordinates for path inside pelican group? NO, legs are inside Pelican group.
// Hip is local inside pelican group.
// Foot is outside.
// We need Foot local to Pelican Group.
// Crank (400, 350). Pelican (340, 140).
// FootGlobal = (400 + px, 350 + py).
// FootLocal = FootGlobal - (340, 140) = (60 + px, 210 + py).
const footLocalX = 60 + px;
const footLocalY = 210 + py;
const kneeLocalX = 60 + kneeNear.x;
const kneeLocalY = 210 + kneeNear.y;
// Draw Near Leg (Thigh to Knee, Shin to Foot)
// d="M hipX hipY L kneeX kneeY L footX footY"
// Note: Hip in pelican group is roughly (30, 40).
els.legNear.setAttribute("d", `M30,40 L${kneeLocalX},${kneeLocalY} L${footLocalX},${footLocalY}`);
// Draw Foot
// Simple line sticking out from foot
els.footNear.setAttribute("d", `M${footLocalX},${footLocalY} L${footLocalX - 15},${footLocalY + 5}`);
// Far Leg (Left Leg)
// Offset Hip slightly for depth
const hipXFar = 10;
const hipYFar = -170;
// Foot left is opposite
const pxLeft = -px;
const pyLeft = -py;
const footLocalX_L = 60 + pxLeft;
const footLocalY_L = 210 + pyLeft;
const kneeFar = solveIK(hipXFar, hipYFar, pxLeft, pyLeft, CONFIG.legLengthThigh, CONFIG.legLengthShin);
// Knee Far Global? No, local.
// Crank center to Pelican origin math applies to knee offset too.
els.legFar.setAttribute("d", `M30,40 L${60 + kneeFar.x},${210 + kneeFar.y} L${footLocalX_L},${footLocalY_L}`);
els.footFar.setAttribute("d", `M${footLocalX_L},${footLocalY_L} L${footLocalX_L - 15},${footLocalY_L + 5}`);
// 3. Body Movement (Bounce & Sway)
const bounce = Math.sin(state.time * 4) * 2;
const sway = Math.sin(state.time * 2) * 1;
// Pelican bounce
els.pelican.setAttribute("transform", `translate(${340 + sway}, ${140 + bounce}) rotate(${sway * 0.5})`);
// Neck bounce (inverse slightly for lag)
const neckLag = Math.sin(state.time * 4 - 1) * 1;
// Update neck path control point
els.neck.setAttribute("d", `M50,20 Q${60 + neckLag},50 ${60 + sway},80 L${60 + sway},${90 + bounce*0.2}`);
// Head rotation
const head = document.getElementById('head-group');
head.setAttribute("transform", `translate(${60 + sway}, ${90 + bounce}) rotate(${sway})`);
// Pouch flap (wind effect based on speed)
const pouchFlap = Math.sin(state.time * 10) * (state.speed / 5);
els.pouch.setAttribute("d", `M15,12 Q50,${40 + pouchFlap} 85,10 Q80,5 60,10`);
// 4. Background Parallax & Clouds
state.clouds.forEach(c => {
c.x -= c.speed * (state.speed / 10 + 0.5); // Move clouds based on speed base + extra
if (c.x < -150) c.x = 850;
c.el.setAttribute("transform", `translate(${c.x}, ${c.y}) scale(${c.scale})`);
});
// 5. Streamers
const streamerWave = Math.sin(state.time * 15) * 5 * (state.speed/20);
els.streamer1.setAttribute("d", `M560,150 Q580,${140 + streamerWave} 590,${160 - streamerWave} T610,${150 + streamerWave}`);
els.streamer2.setAttribute("d", `M560,150 Q580,${160 - streamerWave} 590,${140 + streamerWave} T610,${160 - streamerWave}`);
requestAnimationFrame(animate);
}
// --- Event Listeners ---
// Slider
els.speedSlider.addEventListener('input', (e) => {
state.speed = parseInt(e.target.value);
els.speedDisplay.innerText = state.speed + " km/h";
// Adjust animation speed/duration if needed, but frame-based math handles it mostly.
});
// Theme Toggle
els.themeToggle.addEventListener('click', () => {
state.isNight = !state.isNight;
const btn = els.themeToggle;
const circle = els.toggleCircle;
if (state.isNight) {
btn.classList.remove('bg-sky-200');
btn.classList.add('bg-slate-700');
circle.classList.add('translate-x-6');
circle.innerText = '🌙';
els.skyBg.classList.remove('from-sky-300', 'via-sky-100', 'to-white');
els.skyBg.classList.add('from-indigo-900', 'via-purple-900', 'to-slate-900');
// Darken SVG elements (simple filter or color swap)
document.getElementById('scene').style.filter = "brightness(0.7) contrast(1.2)";
} else {
btn.classList.add('bg-sky-200');
btn.classList.remove('bg-slate-700');
circle.classList.remove('translate-x-6');
circle.innerText = '☀️';
els.skyBg.classList.add('from-sky-300', 'via-sky-100', 'to-white');
els.skyBg.classList.remove('from-indigo-900', 'via-purple-900', 'to-slate-900');
document.getElementById('scene').style.filter = "none";
}
});
// Start Animation
animate();
});
</script>
</body>
</html>
```