我愿称之为最强鹈鹕骑车
此用户已被封禁
2026-08-19 12:02
1
提示词
画一个醍醐骑自行车的动画, html格式, 不需要测试
模型是 Gemini 3.7 flash
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>醍醐兜风记 · Pelican Bicycle Ride</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=Fredoka:wght@400;600;700&family=Plus+Jakarta+Sans:wght@500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-gradient-day: linear-gradient(180deg, #38bdf8 0%, #7dd3fc 35%, #bae6fd 60%, #e0f2fe 100%);
--surface-glass: rgba(255, 255, 255, 0.82);
--surface-glass-border: rgba(255, 255, 255, 0.6);
--text-main: #0f172a;
--text-muted: #475569;
--brand-coral: #f97316;
--brand-coral-hover: #ea580c;
--brand-teal: #0d9488;
--brand-yellow: #facc15;
--shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 16px 32px -4px rgba(15, 23, 42, 0.12), 0 6px 12px -4px rgba(15, 23, 42, 0.08);
--radius-pill: 9999px;
--radius-card: 20px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
body {
font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;
background-color: #0f172a;
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
padding: 16px;
}
.app-container {
width: 100%;
max-width: 1100px;
background: #ffffff;
border-radius: 28px;
box-shadow: var(--shadow-lg);
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.8);
position: relative;
}
/* Header Bar */
.app-header {
padding: 18px 28px;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f1f5f9;
z-index: 10;
}
.brand-title {
display: flex;
align-items: center;
gap: 12px;
}
.brand-icon {
width: 42px;
height: 42px;
background: #ffedd5;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
box-shadow: inset 0 2px 4px rgba(249, 115, 22, 0.15);
}
.brand-text h1 {
font-family: 'Fredoka', cursive, sans-serif;
font-size: 1.35rem;
font-weight: 700;
color: #0f172a;
letter-spacing: -0.01em;
line-height: 1.2;
}
.brand-text p {
font-size: 0.8rem;
color: #64748b;
font-weight: 500;
}
.header-badges {
display: flex;
align-items: center;
gap: 10px;
}
.speedometer-badge {
display: flex;
align-items: baseline;
gap: 4px;
background: #f8fafc;
border: 1px solid #e2e8f0;
padding: 6px 14px;
border-radius: var(--radius-pill);
font-variant-numeric: tabular-nums;
}
.speedometer-num {
font-family: 'Fredoka', cursive, sans-serif;
font-size: 1.25rem;
font-weight: 700;
color: var(--brand-coral);
}
.speedometer-unit {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
/* Canvas Stage */
.canvas-stage {
position: relative;
width: 100%;
height: 520px;
background: #38bdf8;
overflow: hidden;
}
#animCanvas {
width: 100%;
height: 100%;
display: block;
cursor: grab;
}
#animCanvas:active {
cursor: grabbing;
}
/* Floating Canvas Overlays */
.floating-ui-layer {
position: absolute;
top: 16px;
left: 16px;
right: 16px;
display: flex;
justify-content: space-between;
pointer-events: none;
z-index: 5;
}
.floating-pill-group {
display: flex;
gap: 8px;
pointer-events: auto;
}
.glass-btn {
background: var(--surface-glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--surface-glass-border);
padding: 8px 14px;
border-radius: var(--radius-pill);
font-size: 0.82rem;
font-weight: 600;
color: #1e293b;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
box-shadow: var(--shadow-sm);
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.glass-btn:hover {
background: #ffffff;
transform: translateY(-2px);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.1);
}
.glass-btn:active {
transform: translateY(0);
}
.glass-btn.active {
background: #0f172a;
color: #ffffff;
border-color: #0f172a;
}
/* Action Trigger Overlay for Bell & Fish */
.quick-triggers {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 5;
}
.big-action-btn {
background: #ffffff;
border: none;
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
cursor: pointer;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
}
.big-action-btn:hover {
transform: scale(1.1) rotate(5deg);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
}
.big-action-btn:active {
transform: scale(0.95);
}
.big-action-btn .tooltip {
position: absolute;
right: 68px;
background: #0f172a;
color: #fff;
font-size: 0.75rem;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.big-action-btn:hover .tooltip {
opacity: 1;
}
/* Bottom Control Dashboard */
.control-dashboard {
padding: 22px 28px;
background: #ffffff;
border-top: 1px solid #f1f5f9;
display: grid;
grid-template-columns: 1fr 1.2fr 1fr;
gap: 24px;
align-items: center;
}
@media (max-width: 860px) {
.control-dashboard {
grid-template-columns: 1fr;
gap: 18px;
}
.canvas-stage {
height: 400px;
}
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-label {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Gear Shift Segmented Switch */
.segmented-control {
display: flex;
background: #f1f5f9;
padding: 4px;
border-radius: var(--radius-pill);
gap: 2px;
}
.seg-btn {
flex: 1;
border: none;
background: transparent;
padding: 8px 12px;
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
color: #475569;
border-radius: var(--radius-pill);
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
white-space: nowrap;
}
.seg-btn.active {
background: #ffffff;
color: var(--brand-coral);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
font-weight: 700;
}
/* Accessory Selector */
.accessory-buttons {
display: flex;
gap: 8px;
}
.acc-btn {
flex: 1;
padding: 8px 10px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
color: #334155;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
transition: all 0.15s ease;
}
.acc-btn:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
.acc-btn.active {
background: #ffedd5;
border-color: #fdba74;
color: #c2410c;
font-weight: 700;
}
/* Interaction Tips Footer */
.app-footer {
padding: 12px 28px;
background: #fafaf9;
border-top: 1px solid #f1f5f9;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.78rem;
color: #94a3b8;
}
.shortcut-hints {
display: flex;
gap: 16px;
}
.kbd-shortcut {
display: inline-flex;
align-items: center;
gap: 4px;
}
kbd {
background: #e2e8f0;
color: #334155;
padding: 2px 6px;
border-radius: 4px;
font-family: inherit;
font-size: 0.7rem;
font-weight: 700;
box-shadow: 0 1px 0 #cbd5e1;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="app-header">
<div class="brand-title">
<div class="brand-icon">🚴♂️</div>
<div class="brand-text">
<h1>醍醐兜风记 · Pelican Cruise</h1>
<p>海滨公路上的快乐骑手 · Animated Canvas Physics</p>
</div>
</div>
<div class="header-badges">
<div class="speedometer-badge">
<span class="speedometer-num" id="speedDisplay">24</span>
<span class="speedometer-unit">KM/H</span>
</div>
</div>
</header>
<!-- Canvas Animation Area -->
<div class="canvas-stage">
<!-- Floating Canvas UI -->
<div class="floating-ui-layer">
<!-- Environment Themes -->
<div class="floating-pill-group">
<button class="glass-btn active" id="btnThemeDay" onclick="setTheme('day')">☀️ 晴空碧海</button>
<button class="glass-btn" id="btnThemeSunset" onclick="setTheme('sunset')">🌅 黄金日落</button>
<button class="glass-btn" id="btnThemeNight" onclick="setTheme('night')">🌙 萤光星夜</button>
</div>
<!-- Audio & Cam Controls -->
<div class="floating-pill-group">
<button class="glass-btn" id="btnSound" onclick="toggleSound()">🔇 伴音: 关</button>
</div>
</div>
<!-- Quick Action Buttons -->
<div class="quick-triggers">
<button class="big-action-btn" onclick="triggerBell()" title="按空格或点击按铃">
🔔
<span class="tooltip">车铃铛 (Space)</span>
</button>
<button class="big-action-btn" onclick="feedFish()" title="投喂小鱼">
🐟
<span class="tooltip">投喂大鱼 (F)</span>
</button>
</div>
<canvas id="animCanvas"></canvas>
</div>
<!-- Bottom Controls -->
<div class="control-dashboard">
<!-- Gear / Speed Control -->
<div class="control-group">
<div class="control-label">
<span>骑行档位 / 速度</span>
<span id="gearLabel">2 档 (匀速)</span>
</div>
<div class="segmented-control">
<button class="seg-btn" onclick="setGear(1)">1 慢悠</button>
<button class="seg-btn active" onclick="setGear(2)">2 畅骑</button>
<button class="seg-btn" onclick="setGear(3)">3 冲刺</button>
<button class="seg-btn" onclick="setGear(4)">4 狂飙 🚀</button>
</div>
</div>
<!-- Outfits & Hats -->
<div class="control-group">
<div class="control-label">
<span>鹈鹕骑行装束</span>
</div>
<div class="accessory-buttons">
<button class="acc-btn active" id="hatGoggles" onclick="setAccessory('goggles')">🥽 护目镜</button>
<button class="acc-btn" id="hatBeret" onclick="setAccessory('beret')">🎨 法式贝雷帽</button>
<button class="acc-btn" id="hatParty" onclick="setAccessory('party')">🎉 派对尖帽</button>
<button class="acc-btn" id="hatNone" onclick="setAccessory('none')">✨ 原生态</button>
</div>
</div>
<!-- Pelican Mood & Actions -->
<div class="control-group">
<div class="control-label">
<span>互动特效</span>
</div>
<div class="accessory-buttons">
<button class="acc-btn" onclick="pelicanWave()">👋 挥动翅膀</button>
<button class="acc-btn" onclick="pelicanGulp()">😋 抖抖喉囊</button>
</div>
</div>
</div>
<!-- Footer -->
<footer class="app-footer">
<div class="shortcut-hints">
<span class="kbd-shortcut"><kbd>Space</kbd> 响铃</span>
<span class="kbd-shortcut"><kbd>F</kbd> 投鱼</span>
<span class="kbd-shortcut"><kbd>1-4</kbd> 切档</span>
</div>
<div>
<span>纯原生 Canvas 2D + 反向运动学 (IK) 骨骼驱动</span>
</div>
</footer>
</div>
<script>
/* ==========================================================================
1. SOUND & AUDIO SYNTHESIS (Web Audio API - No External Files)
========================================================================== */
let audioCtx = null;
let soundEnabled = false;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
}
function toggleSound() {
initAudio();
soundEnabled = !soundEnabled;
const btn = document.getElementById('btnSound');
if (soundEnabled) {
btn.innerHTML = '🔊 伴音: 开';
btn.classList.add('active');
playBellSound();
} else {
btn.innerHTML = '🔇 伴音: 关';
btn.classList.remove('active');
}
}
function playBellSound() {
if (!soundEnabled || !audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
// Dual-tone chime for realistic bicycle brass bell (Ding-Ding!)
[2400, 3100].forEach((freq, idx) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + idx * 0.08);
gain.gain.setValueAtTime(0, now + idx * 0.08);
gain.gain.linearRampToValueAtTime(0.25, now + idx * 0.08 + 0.005);
gain.gain.exponentialRampToValueAtTime(0.0001, now + idx * 0.08 + 0.45);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now + idx * 0.08);
osc.stop(now + idx * 0.08 + 0.5);
});
}
function playGulpSound() {
if (!soundEnabled || !audioCtx) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(450, now);
osc.frequency.exponentialRampToValueAtTime(180, now + 0.22);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.22);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.25);
}
/* ==========================================================================
2. CANVAS SETUP & RESPONSIVE RETINA SCALING
========================================================================== */
const canvas = document.getElementById('animCanvas');
const ctx = canvas.getContext('2d');
let width = 0;
let height = 0;
let dpr = 1;
function resize() {
const rect = canvas.getBoundingClientRect();
dpr = window.devicePixelRatio || 1;
width = rect.width;
height = rect.height;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
}
window.addEventListener('resize', resize);
/* ==========================================================================
3. STATE MANAGEMENT & PARAMS
========================================================================== */
const state = {
theme: 'day', // 'day' | 'sunset' | 'night'
gear: 2,
targetSpeed: 24,
currentSpeed: 24,
crankAngle: 0,
wheelAngle: 0,
worldDist: 0,
accessory: 'goggles', // 'goggles' | 'beret' | 'party' | 'none'
waveWing: 0,
pouchJiggle: 0,
bellRingAnim: 0,
beakOpen: 0,
fish: [],
particles: [],
clouds: [],
seagulls: [],
trees: [],
dolphins: []
};
// Initialize decorative scenery elements
function initScenery() {
// Clouds
state.clouds = [];
for (let i = 0; i < 8; i++) {
state.clouds.push({
x: Math.random() * 1200,
y: 30 + Math.random() * 110,
scale: 0.6 + Math.random() * 0.8,
speedMult: 0.15 + Math.random() * 0.2
});
}
// Seagulls
state.seagulls = [];
for (let i = 0; i < 4; i++) {
state.seagulls.push({
x: Math.random() * 1000,
y: 40 + Math.random() * 90,
speed: 1.2 + Math.random() * 0.8,
wingPhase: Math.random() * Math.PI * 2
});
}
// Coastal palm trees and bushes
state.trees = [];
for (let i = 0; i < 10; i++) {
state.trees.push({
x: i * 140 + Math.random() * 40,
type: Math.random() > 0.4 ? 'palm' : 'pine',
size: 0.75 + Math.random() * 0.5
});
}
// Jumping dolphin in the sea!
state.dolphins = [
{ x: 300, y: 0, jumpTimer: 0, isJumping: false }
];
}
initScenery();
/* ==========================================================================
4. PALETTES & THEMES
========================================================================== */
const themes = {
day: {
skyTop: '#38bdf8',
skyMid: '#bae6fd',
skyBottom: '#e0f2fe',
seaFar: '#0284c7',
seaNear: '#38bdf8',
mountain: '#7dd3fc',
mountainFront: '#60a5fa',
road: '#64748b',
roadLine: '#f8fafc',
grass: '#10b981',
sunColor: '#fef08a',
sunGlow: 'rgba(254, 240, 138, 0.4)',
ambientLight: 1.0
},
sunset: {
skyTop: '#4c1d95',
skyMid: '#c026d3',
skyBottom: '#fdba74',
seaFar: '#431407',
seaNear: '#ea580c',
mountain: '#701a75',
mountainFront: '#831843',
road: '#475569',
roadLine: '#fde047',
grass: '#b45309',
sunColor: '#ffedd5',
sunGlow: 'rgba(249, 115, 22, 0.6)',
ambientLight: 0.95
},
night: {
skyTop: '#030712',
skyMid: '#0f172a',
skyBottom: '#1e1b4b',
seaFar: '#090d16',
seaNear: '#1e293b',
mountain: '#111827',
mountainFront: '#1f2937',
road: '#334155',
roadLine: '#94a3b8',
grass: '#064e3b',
sunColor: '#f8fafc',
sunGlow: 'rgba(248, 250, 252, 0.25)',
ambientLight: 0.6
}
};
function setTheme(t) {
state.theme = t;
['btnThemeDay', 'btnThemeSunset', 'btnThemeNight'].forEach(id => {
document.getElementById(id).classList.remove('active');
});
if (t === 'day') document.getElementById('btnThemeDay').classList.add('active');
if (t === 'sunset') document.getElementById('btnThemeSunset').classList.add('active');
if (t === 'night') document.getElementById('btnThemeNight').classList.add('active');
}
function setGear(g) {
state.gear = g;
const speeds = { 1: 14, 2: 24, 3: 40, 4: 68 };
const labels = { 1: '1 档 (慢悠)', 2: '2 档 (畅骑)', 3: '3 档 (冲刺)', 4: '4 档 (狂飙 🚀)' };
state.targetSpeed = speeds[g];
document.getElementById('gearLabel').textContent = labels[g];
document.querySelectorAll('.seg-btn').forEach((b, idx) => {
b.classList.toggle('active', idx + 1 === g);
});
}
function setAccessory(acc) {
state.accessory = acc;
['hatGoggles', 'hatBeret', 'hatParty', 'hatNone'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.remove('active');
});
const activeId = 'hat' + acc.charAt(0).toUpperCase() + acc.slice(1);
const activeEl = document.getElementById(activeId);
if (activeEl) activeEl.classList.add('active');
}
function triggerBell() {
state.bellRingAnim = 1.0;
playBellSound();
spawnRingingWaves();
}
function feedFish() {
// Spawn a golden jumping fish from right to left into pelican beak
state.fish.push({
x: width + 50,
y: height * 0.55,
vx: -6.5 - state.currentSpeed * 0.08,
vy: -9.5,
gravity: 0.38,
rotation: 0,
swallowed: false,
scale: 1.0
});
}
function pelicanWave() {
state.waveWing = 1.0;
}
function pelicanGulp() {
state.pouchJiggle = 1.0;
playGulpSound();
}
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
triggerBell();
} else if (e.code === 'KeyF') {
e.preventDefault();
feedFish();
} else if (e.key >= '1' && e.key <= '4') {
setGear(parseInt(e.key));
}
});
/* ==========================================================================
5. 2-BONE INVERSE KINEMATICS SOLVER (For realistic leg pedaling)
========================================================================== */
function solveIK(hx, hy, px, py, L1, L2, bendForward = true) {
const dx = px - hx;
const dy = py - hy;
let d = Math.hypot(dx, dy);
const maxReach = L1 + L2 - 0.001;
if (d > maxReach) d = maxReach;
if (d < Math.abs(L1 - L2) + 0.001) d = Math.abs(L1 - L2) + 0.001;
const alpha = Math.atan2(dy, dx);
const cosAngle = (L1 * L1 + d * d - L2 * L2) / (2 * L1 * d);
const clampedCos = Math.max(-1, Math.min(1, cosAngle));
const beta = Math.acos(clampedCos);
const kneeAngle = bendForward ? alpha - beta : alpha + beta;
const kx = hx + L1 * Math.cos(kneeAngle);
const ky = hy + L1 * Math.sin(kneeAngle);
return { kx, ky };
}
/* ==========================================================================
6. PARTICLES & VISUAL EFFECTS
========================================================================== */
function spawnDust(x, y) {
state.particles.push({
type: 'dust',
x: x + (Math.random() - 0.5) * 8,
y: y + (Math.random() - 0.5) * 4,
vx: -state.currentSpeed * 0.15 - Math.random() * 2,
vy: -0.5 - Math.random() * 1.5,
radius: 3 + Math.random() * 4,
alpha: 0.6 + Math.random() * 0.3,
color: state.theme === 'night' ? 'rgba(148, 163, 184,' : 'rgba(226, 232, 240,'
});
}
function spawnTurboFlame(x, y) {
for (let i = 0; i < 3; i++) {
state.particles.push({
type: 'flame',
x: x,
y: y + (Math.random() - 0.5) * 8,
vx: -state.currentSpeed * 0.4 - Math.random() * 4,
vy: (Math.random() - 0.5) * 2,
radius: 4 + Math.random() * 6,
alpha: 0.9,
color: Math.random() > 0.5 ? '#f97316' : '#facc15'
});
}
}
function spawnRingingWaves() {
const bikeX = width * 0.42;
const groundY = height * 0.72;
for (let i = 0; i < 3; i++) {
setTimeout(() => {
state.particles.push({
type: 'ring',
x: bikeX + 38,
y: groundY - 110,
radius: 8,
maxRadius: 40 + i * 15,
alpha: 0.9
});
}, i * 70);
}
}
/* ==========================================================================
7. MAIN DRAWING ROUTINES
========================================================================== */
function drawSkyAndEnvironment(t) {
const theme = themes[state.theme];
// Sky Gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, height * 0.65);
skyGrad.addColorStop(0, theme.skyTop);
skyGrad.addColorStop(0.5, theme.skyMid);
skyGrad.addColorStop(1, theme.skyBottom);
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, width, height);
// Celestial Sun / Moon
const sunX = width * 0.8;
const sunY = height * 0.22;
ctx.save();
// Glow
const glowGrad = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 120);
glowGrad.addColorStop(0, theme.sunGlow);
glowGrad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(sunX, sunY, 120, 0, Math.PI * 2);
ctx.fill();
// Sun/Moon Body
ctx.fillStyle = theme.sunColor;
ctx.beginPath();
ctx.arc(sunX, sunY, state.theme === 'night' ? 24 : 32, 0, Math.PI * 2);
ctx.fill();
if (state.theme === 'night') {
// Moon craters
ctx.fillStyle = '#cbd5e1';
ctx.beginPath();
ctx.arc(sunX - 6, sunY - 4, 4, 0, Math.PI * 2);
ctx.arc(sunX + 6, sunY + 6, 6, 0, Math.PI * 2);
ctx.fill();
// Stars
ctx.fillStyle = 'rgba(255,255,255,0.85)';
for (let i = 0; i < 24; i++) {
const sx = (Math.sin(i * 99 + t * 0.0005) * 0.5 + 0.5) * width;
const sy = (Math.cos(i * 33) * 0.5 + 0.5) * (height * 0.4);
const tw = (Math.sin(t * 0.005 + i) * 0.5 + 0.5);
ctx.beginPath();
ctx.arc(sx, sy, 1 + tw * 1.5, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
// Distant Clouds
ctx.save();
state.clouds.forEach(cloud => {
cloud.x -= state.currentSpeed * 0.03 * cloud.speedMult;
if (cloud.x < -160) cloud.x = width + 100;
ctx.fillStyle = state.theme === 'night' ? 'rgba(30, 41, 59, 0.45)' : 'rgba(255, 255, 255, 0.7)';
ctx.beginPath();
const cx = cloud.x;
const cy = cloud.y;
const s = cloud.scale;
ctx.arc(cx, cy, 24 * s, 0, Math.PI * 2);
ctx.arc(cx + 25 * s, cy - 10 * s, 28 * s, 0, Math.PI * 2);
ctx.arc(cx + 55 * s, cy, 22 * s, 0, Math.PI * 2);
ctx.arc(cx + 30 * s, cy + 10 * s, 30 * s, 0, Math.PI * 2);
ctx.fill();
});
ctx.restore();
// Distant Mountain Ranges
const horizonY = height * 0.55;
ctx.fillStyle = theme.mountain;
ctx.beginPath();
ctx.moveTo(0, horizonY);
for (let x = 0; x <= width; x += 30) {
const offset = (state.worldDist * 0.04 + x * 0.004);
const my = horizonY - 45 - Math.sin(offset) * 35 - Math.cos(offset * 2.2) * 20;
ctx.lineTo(x, my);
}
ctx.lineTo(width, horizonY);
ctx.closePath();
ctx.fill();
// Foreground Hills
ctx.fillStyle = theme.mountainFront;
ctx.beginPath();
ctx.moveTo(0, horizonY + 10);
for (let x = 0; x <= width; x += 25) {
const offset = (state.worldDist * 0.08 + x * 0.007);
const my = horizonY - 15 - Math.sin(offset * 1.5) * 25 - Math.sin(offset * 0.5) * 15;
ctx.lineTo(x, my);
}
ctx.lineTo(width, horizonY + 10);
ctx.closePath();
ctx.fill();
// Ocean Layer with waves
const seaGrad = ctx.createLinearGradient(0, horizonY, 0, height * 0.72);
seaGrad.addColorStop(0, theme.seaFar);
seaGrad.addColorStop(1, theme.seaNear);
ctx.fillStyle = seaGrad;
ctx.fillRect(0, horizonY, width, height * 0.17);
// Sea wave glints
ctx.strokeStyle = state.theme === 'sunset' ? 'rgba(253, 186, 116, 0.4)' : 'rgba(255, 255, 255, 0.35)';
ctx.lineWidth = 1.5;
for (let i = 0; i < 12; i++) {
const waveX = ((i * 110 - state.worldDist * 0.2) % (width + 200)) - 100;
const waveY = horizonY + 8 + (i % 4) * 8;
ctx.beginPath();
ctx.moveTo(waveX, waveY);
ctx.quadraticCurveTo(waveX + 15, waveY - 3, waveX + 30, waveY);
ctx.stroke();
}
// Coastal Grass/Soil Bank
const groundY = height * 0.72;
ctx.fillStyle = theme.grass;
ctx.fillRect(0, groundY - 18, width, 24);
// Scrolling Palm / Pine trees along coast
state.trees.forEach(tree => {
const screenX = ((tree.x - state.worldDist * 0.4) % (width + 200));
const tx = screenX < -100 ? screenX + width + 200 : screenX;
const ty = groundY - 14;
ctx.save();
ctx.translate(tx, ty);
ctx.scale(tree.size, tree.size);
if (tree.type === 'palm') {
// Curved trunk
ctx.strokeStyle = state.theme === 'night' ? '#1e293b' : '#78350f';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(-10, -35, -5, -70);
ctx.stroke();
// Palm fronds
ctx.fillStyle = state.theme === 'night' ? '#064e3b' : '#059669';
for (let f = 0; f < 5; f++) {
const angle = (f - 2) * 0.55 - 0.2;
ctx.save();
ctx.translate(-5, -70);
ctx.rotate(angle);
ctx.beginPath();
ctx.ellipse(22, 0, 24, 7, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
} else {
// Pine tree
ctx.fillStyle = state.theme === 'night' ? '#0f172a' : '#5c3a21';
ctx.fillRect(-3, -15, 6, 15);
ctx.fillStyle = state.theme === 'night' ? '#064e3b' : '#047857';
for (let p = 0; p < 3; p++) {
ctx.beginPath();
ctx.moveTo(0, -50 + p * 12);
ctx.lineTo(-18 + p * 3, -25 + p * 12);
ctx.lineTo(18 - p * 3, -25 + p * 12);
ctx.closePath();
ctx.fill();
}
}
ctx.restore();
});
// Road Surface
ctx.fillStyle = theme.road;
ctx.fillRect(0, groundY, width, height - groundY);
// Curb edge
ctx.fillStyle = '#94a3b8';
ctx.fillRect(0, groundY - 2, width, 4);
// Dashed Road Line
ctx.fillStyle = theme.roadLine;
const dashWidth = 45;
const dashGap = 35;
const cycle = dashWidth + dashGap;
const dashOffset = (state.worldDist * 0.8) % cycle;
const roadLineY = groundY + (height - groundY) * 0.45;
for (let dx = -dashOffset; dx < width + cycle; dx += cycle) {
ctx.fillRect(dx, roadLineY, dashWidth, 6);
}
// Flying Seagulls
ctx.strokeStyle = state.theme === 'night' ? '#94a3b8' : '#ffffff';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
state.seagulls.forEach(sg => {
sg.x -= (state.currentSpeed * 0.05 + sg.speed);
if (sg.x < -40) sg.x = width + 60;
sg.wingPhase += 0.12;
const flap = Math.sin(sg.wingPhase) * 6;
ctx.beginPath();
ctx.moveTo(sg.x - 12, sg.y + flap);
ctx.quadraticCurveTo(sg.x - 6, sg.y - 4, sg.x, sg.y);
ctx.quadraticCurveTo(sg.x + 6, sg.y - 4, sg.x + 12, sg.y + flap);
ctx.stroke();
});
}
/* ==========================================================================
8. DRAW BICYCLE & MECHANICAL DETAIL
========================================================================== */
function drawBicycle(bx, by, crankAngle, wheelAngle) {
// Dimensions
const wheelbase = 160;
const rearX = bx - wheelbase * 0.5;
const frontX = bx + wheelbase * 0.5;
const wheelR = 40;
const bbX = bx; // Bottom Bracket (crank center)
const bbY = by - 16;
const seatX = bx - 26;
const seatY = by - 70;
const headX = bx + 48;
const headY = by - 80;
ctx.save();
// Drop shadows under wheels
ctx.fillStyle = 'rgba(15, 23, 42, 0.28)';
ctx.beginPath();
ctx.ellipse(rearX, by + 1, 42, 7, 0, 0, Math.PI * 2);
ctx.ellipse(frontX, by + 1, 42, 7, 0, 0, Math.PI * 2);
ctx.fill();
// Function to draw realistic spoke wheels
function drawWheel(wx, wy, rot) {
ctx.save();
ctx.translate(wx, wy);
ctx.rotate(rot);
// Tire Outer Rubber
ctx.strokeStyle = '#1e293b';
ctx.lineWidth = 9;
ctx.beginPath();
ctx.arc(0, 0, wheelR, 0, Math.PI * 2);
ctx.stroke();
// Shiny Chrome Rim
ctx.strokeStyle = '#cbd5e1';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(0, 0, wheelR - 3, 0, Math.PI * 2);
ctx.stroke();
// Spokes
ctx.strokeStyle = 'rgba(226, 232, 240, 0.85)';
ctx.lineWidth = 1.2;
const numSpokes = 12;
for (let i = 0; i < numSpokes; i++) {
const a = (i / numSpokes) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(a) * (wheelR - 4), Math.sin(a) * (wheelR - 4));
ctx.stroke();
}
// Center Hub
ctx.fillStyle = '#475569';
ctx.beginPath();
ctx.arc(0, 0, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#e2e8f0';
ctx.beginPath();
ctx.arc(0, 0, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Draw Wheels
drawWheel(rearX, by - wheelR, wheelAngle);
drawWheel(frontX, by - wheelR, wheelAngle);
// Bicycle Frame Tubing (Vintage Emerald / Mint Teal)
const frameColor = '#0d9488';
const frameAccent = '#14b8a6';
ctx.lineWidth = 7;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = frameColor;
// Rear Triangle (Chainstay & Seatstay)
ctx.beginPath();
ctx.moveTo(rearX, by - wheelR);
ctx.lineTo(bbX, bbY); // Chainstay
ctx.lineTo(seatX, seatY + 12); // Seat tube
ctx.lineTo(rearX, by - wheelR); // Seatstay
ctx.stroke();
// Front Triangle (Main Frame: Down tube & Top tube)
ctx.beginPath();
ctx.moveTo(bbX, bbY);
ctx.lineTo(headX, headY + 14); // Down tube
ctx.lineTo(seatX, seatY + 12); // Top tube
ctx.stroke();
// Front Fork
ctx.strokeStyle = frameAccent;
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(headX, headY + 14);
ctx.lineTo(frontX, by - wheelR);
ctx.stroke();
// Seatpost & Leather Saddle
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(seatX, seatY + 12);
ctx.lineTo(seatX - 4, seatY);
ctx.stroke();
// Comfy Vintage Saddle
ctx.fillStyle = '#78350f';
ctx.beginPath();
ctx.roundRect(seatX - 22, seatY - 5, 34, 10, [6, 8, 4, 4]);
ctx.fill();
ctx.fillStyle = '#92400e';
ctx.beginPath();
ctx.arc(seatX - 16, seatY - 1, 4, 0, Math.PI * 2);
ctx.fill();
// Stem & Drop / Cruiser Handlebars
ctx.strokeStyle = '#cbd5e1';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(headX, headY + 14);
ctx.lineTo(headX + 2, headY - 12); // Stem
ctx.stroke();
// Handlebar curve
ctx.strokeStyle = '#e2e8f0';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(headX - 14, headY - 14);
ctx.quadraticCurveTo(headX + 8, headY - 18, headX + 16, headY - 6);
ctx.stroke();
// Handlebar Grips
ctx.fillStyle = '#78350f';
ctx.beginPath();
ctx.arc(headX + 16, headY - 6, 5, 0, Math.PI * 2);
ctx.fill();
// Front Brass Bell
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.arc(headX + 6, headY - 20, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ca8a04';
ctx.beginPath();
ctx.arc(headX + 6, headY - 20, 3, 0, Math.PI * 2);
ctx.fill();
// Bell Vibration Ring Waves if ringing
if (state.bellRingAnim > 0) {
ctx.strokeStyle = `rgba(250, 204, 21, ${state.bellRingAnim})`;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(headX + 6, headY - 20, 10 + (1 - state.bellRingAnim) * 18, 0, Math.PI * 2);
ctx.stroke();
}
// Front Basket with Flower / Baguette
ctx.fillStyle = '#d97706';
ctx.lineWidth = 2;
ctx.strokeStyle = '#b45309';
ctx.beginPath();
ctx.roundRect(headX + 14, headY - 12, 24, 18, 4);
ctx.fill();
ctx.stroke();
// Sunflower in basket
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.arc(headX + 26, headY - 14, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#78350f';
ctx.beginPath();
ctx.arc(headX + 26, headY - 14, 3, 0, Math.PI * 2);
ctx.fill();
// Vintage Chrome Headlight (Glows at night/sunset!)
ctx.fillStyle = '#e2e8f0';
ctx.beginPath();
ctx.arc(headX + 22, headY + 4, 8, -Math.PI * 0.5, Math.PI * 0.5);
ctx.fill();
if (state.theme === 'night' || state.theme === 'sunset') {
// Headlight Light Beam Cone
const beamGrad = ctx.createRadialGradient(headX + 24, headY + 4, 5, headX + 160, headY + 30, 180);
beamGrad.addColorStop(0, 'rgba(254, 240, 138, 0.65)');
beamGrad.addColorStop(1, 'rgba(254, 240, 138, 0.0)');
ctx.fillStyle = beamGrad;
ctx.beginPath();
ctx.moveTo(headX + 24, headY + 4);
ctx.lineTo(headX + 260, headY - 40);
ctx.lineTo(headX + 290, headY + 90);
ctx.closePath();
ctx.fill();
}
// Sprocket & Chain
ctx.fillStyle = '#64748b';
ctx.beginPath();
ctx.arc(bbX, bbY, 13, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(bbX, bbY - 13);
ctx.lineTo(rearX, by - wheelR - 6);
ctx.lineTo(rearX, by - wheelR + 6);
ctx.lineTo(bbX, bbY + 13);
ctx.closePath();
ctx.stroke();
ctx.restore();
return { bbX, bbY, seatX, seatY, headX, headY };
}
/* ==========================================================================
9. DRAW PELICAN CHARACTER (With IK Legs, Pouch Physics, Hat & Expressions)
========================================================================== */
function drawPelican(bikePoints, crankAngle, t) {
const { bbX, bbY, seatX, seatY, headX, headY } = bikePoints;
// Cadence bounce
const bounce = Math.sin(crankAngle * 2) * 3;
const pelicanHipX = seatX + 2;
const pelicanHipY = seatY - 14 + bounce;
const crankR = 20;
// Pedal positions
const rightPedalX = bbX + Math.cos(crankAngle) * crankR;
const rightPedalY = bbY + Math.sin(crankAngle) * crankR;
const leftPedalX = bbX + Math.cos(crankAngle + Math.PI) * crankR;
const leftPedalY = bbY + Math.sin(crankAngle + Math.PI) * crankR;
const thighL = 34;
const shinL = 32;
ctx.save();
// --- 1. LEFT (FAR) LEG & PEDAL CRANK ---
// Left Crank Arm
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(bbX, bbY);
ctx.lineTo(leftPedalX, leftPedalY);
ctx.stroke();
// Left Pedal Flat
ctx.fillStyle = '#334155';
ctx.fillRect(leftPedalX - 7, leftPedalY - 3, 14, 6);
// Solve IK for Left Leg
const leftLegIK = solveIK(pelicanHipX - 4, pelicanHipY, leftPedalX, leftPedalY, thighL, shinL, true);
// Far Leg (Darker orange for depth)
ctx.strokeStyle = '#ea580c';
ctx.lineWidth = 8;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(pelicanHipX - 4, pelicanHipY);
ctx.lineTo(leftLegIK.kx, leftLegIK.ky);
ctx.lineTo(leftPedalX, leftPedalY);
ctx.stroke();
// Webbed Foot (Far)
ctx.fillStyle = '#c2410c';
ctx.beginPath();
ctx.ellipse(leftPedalX + 4, leftPedalY, 10, 5, 0.1, 0, Math.PI * 2);
ctx.fill();
// --- 2. PELICAN BODY, CHEST & TAIL ---
const bodyBob = bounce * 0.7;
const bodyCenterY = pelicanHipY - 26 + bodyBob;
const bodyCenterX = pelicanHipX + 10;
// Tail feathers (wagging in the wind)
const tailWag = Math.sin(t * 0.015 + state.currentSpeed * 0.05) * 6;
ctx.fillStyle = '#e2e8f0';
ctx.beginPath();
ctx.moveTo(bodyCenterX - 22, bodyCenterY);
ctx.lineTo(bodyCenterX - 45, bodyCenterY - 12 + tailWag);
ctx.lineTo(bodyCenterX - 40, bodyCenterY + 2 + tailWag);
ctx.lineTo(bodyCenterX - 46, bodyCenterY + 12 + tailWag);
ctx.lineTo(bodyCenterX - 18, bodyCenterY + 16);
ctx.closePath();
ctx.fill();
// Main Plump Body (Pure white with soft shading)
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.ellipse(bodyCenterX, bodyCenterY, 28, 22, -0.22, 0, Math.PI * 2);
ctx.fill();
// Belly fluff shadow
ctx.fillStyle = '#f1f5f9';
ctx.beginPath();
ctx.ellipse(bodyCenterX + 2, bodyCenterY + 6, 22, 14, -0.2, 0, Math.PI * 2);
ctx.fill();
// --- 3. CURVED NECK & HEAD ---
const neckBaseX = bodyCenterX + 16;
const neckBaseY = bodyCenterY - 8;
const headCenterY = neckBaseY - 48 + bodyBob * 0.5;
const headCenterX = neckBaseX + 16;
// S-Curved Graceful Pelican Neck
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 22;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(neckBaseX, neckBaseY);
ctx.quadraticCurveTo(neckBaseX + 14, neckBaseY - 25, headCenterX, headCenterY);
ctx.stroke();
// Cute Fluttering Scarf / Bandana around Neck
const scarfWind = Math.sin(t * 0.02 + state.currentSpeed * 0.08) * (8 + state.currentSpeed * 0.2);
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.arc(neckBaseX + 6, neckBaseY - 24, 12, 0, Math.PI * 2);
ctx.fill();
// Scarf tails streaming behind
ctx.fillStyle = '#dc2626';
ctx.beginPath();
ctx.moveTo(neckBaseX + 2, neckBaseY - 22);
ctx.lineTo(neckBaseX - 28, neckBaseY - 30 + scarfWind);
ctx.lineTo(neckBaseX - 22, neckBaseY - 20 + scarfWind);
ctx.lineTo(neckBaseX - 32, neckBaseY - 14 + scarfWind);
ctx.lineTo(neckBaseX + 2, neckBaseY - 16);
ctx.closePath();
ctx.fill();
// Head Base Ball
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(headCenterX, headCenterY, 18, 0, Math.PI * 2);
ctx.fill();
// --- 4. FAMOUS PELICAN BEAK & ELASTIC THROAT POUCH ---
const beakStartX = headCenterX + 8;
const beakStartY = headCenterY + 2;
const beakLength = 62;
const beakTipX = beakStartX + beakLength;
const beakTipY = beakStartY - 2;
// Pouch Stretchy Jiggle Physics
const pouchWind = Math.sin(t * 0.012 + state.currentSpeed * 0.06) * (4 + state.currentSpeed * 0.15);
const jiggle = Math.sin(state.pouchJiggle * Math.PI * 4) * 14 * state.pouchJiggle;
const pouchDrop = 34 + jiggle + (state.beakOpen * 18);
// Huge Elastic Throat Pouch (Bright warm orange/peach)
ctx.fillStyle = '#fb923c';
ctx.strokeStyle = '#ea580c';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(beakStartX - 8, beakStartY + 6);
ctx.quadraticCurveTo(
beakStartX + 18,
beakStartY + pouchDrop + pouchWind,
beakTipX - 4,
beakTipY + (state.beakOpen * 20)
);
ctx.lineTo(beakStartX + 8, beakStartY + (state.beakOpen * 8));
ctx.closePath();
ctx.fill();
ctx.stroke();
// Throat pouch muscle stretch lines
ctx.strokeStyle = 'rgba(234, 88, 12, 0.4)';
ctx.lineWidth = 1.5;
for (let s = 1; s <= 3; s++) {
ctx.beginPath();
ctx.moveTo(beakStartX + s * 10, beakStartY + 4);
ctx.quadraticCurveTo(
beakStartX + s * 11,
beakStartY + (pouchDrop * 0.7) + pouchWind * 0.5,
beakStartX + s * 14,
beakStartY + pouchDrop * 0.95
);
ctx.stroke();
}
// Upper Long Bill (Hard keratin with hooked tip)
ctx.fillStyle = '#f97316';
ctx.strokeStyle = '#ea580c';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(beakStartX - 6, beakStartY - 8);
ctx.lineTo(beakTipX, beakTipY);
// Hook tip at end of bill
ctx.quadraticCurveTo(beakTipX + 6, beakTipY + 4, beakTipX + 2, beakTipY + 8);
ctx.lineTo(beakStartX, beakStartY + 2);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Nostril ridge
ctx.fillStyle = '#ea580c';
ctx.beginPath();
ctx.arc(beakStartX + 8, beakStartY - 4, 2.5, 0, Math.PI * 2);
ctx.fill();
// Pelican Eyes (Big, expressive, cute)
const eyeX = headCenterX + 4;
const eyeY = headCenterY - 6;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(eyeX, eyeY, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#cbd5e1';
ctx.lineWidth = 1;
ctx.stroke();
// Pupil (Looking forward happily)
ctx.fillStyle = '#0f172a';
ctx.beginPath();
ctx.arc(eyeX + 2, eyeY, 4, 0, Math.PI * 2);
ctx.fill();
// Eye sparkle
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(eyeX + 3.5, eyeY - 1.5, 1.5, 0, Math.PI * 2);
ctx.fill();
// Cheerful Blush Cheek
ctx.fillStyle = 'rgba(244, 63, 94, 0.45)';
ctx.beginPath();
ctx.arc(headCenterX - 2, headCenterY + 4, 5, 0, Math.PI * 2);
ctx.fill();
// --- 5. ACCESSORIES & HATS ---
if (state.accessory === 'goggles') {
// Cool Aviator Goggles pushed up on forehead
ctx.fillStyle = '#475569';
ctx.strokeStyle = '#1e293b';
ctx.lineWidth = 2;
// Strap around head
ctx.beginPath();
ctx.moveTo(headCenterX - 14, headCenterY - 10);
ctx.lineTo(headCenterX + 12, headCenterY - 14);
ctx.stroke();
// Dual Lenses
[-4, 8].forEach(gOff => {
ctx.fillStyle = '#38bdf8';
ctx.beginPath();
ctx.arc(headCenterX + gOff, headCenterY - 15, 8, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Lens reflection
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.beginPath();
ctx.arc(headCenterX + gOff - 2, headCenterY - 17, 3, 0, Math.PI * 2);
ctx.fill();
});
} else if (state.accessory === 'beret') {
// French Artist Beret (Navy Blue)
ctx.fillStyle = '#1e3a8a';
ctx.beginPath();
ctx.ellipse(headCenterX - 2, headCenterY - 16, 20, 9, -0.2, 0, Math.PI * 2);
ctx.fill();
// Little top stem
ctx.strokeStyle = '#1e3a8a';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(headCenterX - 4, headCenterY - 24);
ctx.lineTo(headCenterX - 5, headCenterY - 29);
ctx.stroke();
} else if (state.accessory === 'party') {
// Striped Birthday Party Hat
ctx.save();
ctx.translate(headCenterX - 2, headCenterY - 14);
ctx.rotate(-0.15);
ctx.fillStyle = '#f43f5e';
ctx.beginPath();
ctx.moveTo(0, -32);
ctx.lineTo(-12, 0);
ctx.lineTo(12, 0);
ctx.closePath();
ctx.fill();
// Stripes
ctx.strokeStyle = '#facc15';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(-7, -12);
ctx.lineTo(7, -12);
ctx.moveTo(-4, -22);
ctx.lineTo(4, -22);
ctx.stroke();
// Pom pom on top
ctx.fillStyle = '#38bdf8';
ctx.beginPath();
ctx.arc(0, -34, 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// --- 6. WINGS (Hands steering handlebar or waving!) ---
const wingRootX = bodyCenterX + 2;
const wingRootY = bodyCenterY - 8;
const gripX = headX + 14;
const gripY = headY - 8;
if (state.waveWing > 0) {
// High Wave Animation
const waveAngle = Math.sin(t * 0.02) * 0.4 - 0.6;
ctx.save();
ctx.translate(wingRootX, wingRootY);
ctx.rotate(waveAngle);
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#e2e8f0';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(36, -30);
ctx.lineTo(48, -20);
ctx.lineTo(38, 0);
ctx.lineTo(20, 14);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
} else {
// Normal Wing resting firmly on handlebars
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#e2e8f0';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(wingRootX, wingRootY);
ctx.quadraticCurveTo(wingRootX + 16, wingRootY + 12, gripX, gripY);
ctx.lineTo(gripX - 8, gripY + 8);
ctx.quadraticCurveTo(wingRootX + 8, wingRootY + 18, wingRootX - 10, wingRootY + 10);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Feather tips
ctx.strokeStyle = '#cbd5e1';
ctx.beginPath();
ctx.moveTo(wingRootX + 4, wingRootY + 8);
ctx.lineTo(wingRootX + 16, wingRootY + 14);
ctx.stroke();
}
// --- 7. RIGHT (NEAR) LEG & PEDAL CRANK ---
// Right Crank Arm
ctx.strokeStyle = '#cbd5e1';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(bbX, bbY);
ctx.lineTo(rightPedalX, rightPedalY);
ctx.stroke();
// Right Pedal Flat
ctx.fillStyle = '#1e293b';
ctx.fillRect(rightPedalX - 8, rightPedalY - 4, 16, 8);
// Solve IK for Right Leg
const rightLegIK = solveIK(pelicanHipX + 4, pelicanHipY, rightPedalX, rightPedalY, thighL, shinL, true);
// Bright Orange Foreground Leg
ctx.strokeStyle = '#f97316';
ctx.lineWidth = 9;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(pelicanHipX + 4, pelicanHipY);
ctx.lineTo(rightLegIK.kx, rightLegIK.ky);
ctx.lineTo(rightPedalX, rightPedalY);
ctx.stroke();
// Webbed Foot (Near) gripping the pedal
ctx.fillStyle = '#ea580c';
ctx.beginPath();
ctx.ellipse(rightPedalX + 5, rightPedalY + 1, 12, 6, 0.15, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
/* ==========================================================================
10. FLYING FISH FEEDING & PARTICLES UPDATE
========================================================================== */
function updateAndDrawFish(bikePoints) {
const pelicanMouthX = bikePoints.headX + 50;
const pelicanMouthY = bikePoints.headY - 42;
for (let i = state.fish.length - 1; i >= 0; i--) {
const f = state.fish[i];
f.x += f.vx;
f.y += f.vy;
f.vy += f.gravity;
f.rotation += 0.08;
// Check if fish is near mouth
const distToMouth = Math.hypot(f.x - pelicanMouthX, f.y - pelicanMouthY);
if (distToMouth < 45 && !f.swallowed) {
f.swallowed = true;
state.beakOpen = 1.0;
state.pouchJiggle = 1.0;
playGulpSound();
}
if (f.swallowed) {
// Fish enters throat and disappears with a delicious splash
state.fish.splice(i, 1);
continue;
}
// Draw Jumping Fish
ctx.save();
ctx.translate(f.x, f.y);
ctx.rotate(f.rotation);
// Shiny Golden Fish Body
ctx.fillStyle = '#f59e0b';
ctx.strokeStyle = '#b45309';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.ellipse(0, 0, 16, 9, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Fish Tail & Fins
ctx.fillStyle = '#f97316';
ctx.beginPath();
ctx.moveTo(-14, 0);
ctx.lineTo(-24, -8);
ctx.lineTo(-20, 0);
ctx.lineTo(-24, 8);
ctx.closePath();
ctx.fill();
// Cute Fish Eye
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(8, -3, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#0f172a';
ctx.beginPath();
ctx.arc(9, -3, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Remove if off screen bottom
if (f.y > height + 60) {
state.fish.splice(i, 1);
}
}
}
function updateAndDrawParticles() {
for (let i = state.particles.length - 1; i >= 0; i--) {
const p = state.particles[i];
p.x += p.vx || 0;
p.y += p.vy || 0;
p.alpha -= 0.018;
if (p.type === 'ring') {
p.radius += 1.8;
ctx.strokeStyle = `rgba(250, 204, 21, ${p.alpha})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.stroke();
} else if (p.type === 'flame') {
p.radius *= 0.95;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, Math.max(1, p.radius), 0, Math.PI * 2);
ctx.fill();
} else {
// Dust
p.radius *= 0.98;
ctx.fillStyle = `${p.color} ${p.alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, Math.max(1, p.radius), 0, Math.PI * 2);
ctx.fill();
}
if (p.alpha <= 0.01 || p.radius < 0.5) {
state.particles.splice(i, 1);
}
}
}
/* ==========================================================================
11. MASTER ANIMATION LOOP (60FPS Smooth Rendering)
========================================================================== */
let lastTime = performance.now();
function animate(currentTime) {
const dt = Math.min((currentTime - lastTime) / 1000, 0.1);
lastTime = currentTime;
// Smoothly interpolate current speed towards target
state.currentSpeed += (state.targetSpeed - state.currentSpeed) * 0.08;
document.getElementById('speedDisplay').textContent = Math.round(state.currentSpeed);
// Advance physics & angles
const speedFactor = state.currentSpeed * 0.18;
state.crankAngle += speedFactor * dt * 5.2;
state.wheelAngle += speedFactor * dt * 6.8;
state.worldDist += state.currentSpeed * dt * 32;
// Decay interactive animations
if (state.waveWing > 0) state.waveWing = Math.max(0, state.waveWing - dt * 0.8);
if (state.pouchJiggle > 0) state.pouchJiggle = Math.max(0, state.pouchJiggle - dt * 1.5);
if (state.bellRingAnim > 0) state.bellRingAnim = Math.max(0, state.bellRingAnim - dt * 1.8);
if (state.beakOpen > 0) state.beakOpen = Math.max(0, state.beakOpen - dt * 2.5);
// Clear & Draw
ctx.clearRect(0, 0, width, height);
// 1. Environmental Backdrop
drawSkyAndEnvironment(currentTime);
// 2. Bicycle & Pelican Coordinate Anchor
const bikeX = width * 0.42;
const groundY = height * 0.72;
// Spawn dust from rear tire
if (Math.random() < 0.35 * (state.currentSpeed / 20)) {
spawnDust(bikeX - 80, groundY);
}
// Turbo mode flame effects!
if (state.gear === 4) {
spawnTurboFlame(bikeX - 82, groundY - 40);
}
// 3. Draw Bicycle Mechanics
const bikePoints = drawBicycle(bikeX, groundY, state.crankAngle, state.wheelAngle);
// 4. Draw Animated Pelican
drawPelican(bikePoints, state.crankAngle, currentTime);
// 5. Update Flying Fish & Particles
updateAndDrawFish(bikePoints);
updateAndDrawParticles();
requestAnimationFrame(animate);
}
// Initial setup & start loop
resize();
requestAnimationFrame(animate);
</script>
</body>
</html>
(base) yangzhengkai@yangzhengkaideMac-mini ~ % >....
}
// 3. Draw Bicycle Mechanics
const bikePoints = drawBicycle(bikeX, groundY, state.crankAngle, state.wheelAngle);
// 4. Draw Animated Pelican
drawPelican(bikePoints, state.crankAngle, currentTime);
// 5. Update Flying Fish & Particles
updateAndDrawFish(bikePoints);
updateAndDrawParticles();
requestAnimationFrame(animate);
}
// Initial setup & start loop
resize();
requestAnimationFrame(animate);
</script>
</body>
</html>
最新回复 (14)
-
William 08-19 12:041楼是我的网络问题吗,为什么加载不出图片? ^-^
-
Pukii. 08-19 12:042楼本地运行了一下,还蛮有意思的。。
-
Ler1Shy 08-19 12:043楼图裂了 换个图床吧 或者重新传一下
-
泼痞 08-19 12:064楼
是个mp4,怎么可能加载出来 -
云馨 08-19 12:065楼
我帮lz截个图
,看起来还可以哦,就是背景速度好快
有问题的地方:
- 背景速度过快
- 投喂鱼不进嘴里
- 挥挥手有点抽象
-
泰迪 08-19 12:076楼
我嘞个60KM/H的自行车 ^-^ -
小猫曦月 08-19 12:137楼很不错了,只比我之前在站内看到的fable的骑车差一点点,性价比无敌啊
-
wjy 08-19 12:148楼HTML和svg不是一个难度吧。
-
ysin 08-19 12:149楼
画一个醍醐骑自行车的动画, html格式, 不需要测试
用你的提示词让口水4.6跑了一下

-
lililihy 08-19 12:1610楼意思是前端很强?那搭配deepseek flash岂不是很完美
-
sephzl 08-19 12:3011楼gemini 前端有点东西的,我测试的骑摩托车

-
Dopawei 08-19 12:3712楼gemini 3.7 flash这么强吗?还是佬用了什么特定的skills
-
sisSs 08-19 12:4113楼同3.7 ^-^

-
此用户已被封禁 楼主 08-19 12:4114楼刚才链接有问题, 我换了个YouTube链接
* 帖子来源Linux.do
附近帖子