497 lines
18 KiB
HTML
497 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>夏夜蒲公英 - Dandelion Dreams</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
overflow: hidden;
|
|
background: linear-gradient(to bottom,
|
|
#0a0e27 0%,
|
|
#1a1f4d 25%,
|
|
#2d3561 50%,
|
|
#1a2342 75%,
|
|
#0d1229 100%);
|
|
height: 100vh;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
#canvas {
|
|
display: block;
|
|
cursor: crosshair;
|
|
}
|
|
|
|
#info {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
font-size: 14px;
|
|
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
}
|
|
|
|
#info span {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.fps {
|
|
color: #7dd3c0;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.count {
|
|
color: #f0c674;
|
|
}
|
|
|
|
#hint {
|
|
position: absolute;
|
|
bottom: 30px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
color: rgba(255, 255, 255, 0.5);
|
|
font-size: 13px;
|
|
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
|
pointer-events: none;
|
|
animation: pulse 3s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.4; }
|
|
50% { opacity: 0.8; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="info">
|
|
<span class="fps">FPS: <span id="fps">0</span></span>
|
|
<span class="count">绒毛数量: <span id="seedCount">0</span></span>
|
|
</div>
|
|
<div id="hint">移动鼠标触碰绒毛,感受夏夜的微风 ✨</div>
|
|
<canvas id="canvas"></canvas>
|
|
|
|
<script>
|
|
const canvas = document.getElementById('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const fpsDisplay = document.getElementById('fps');
|
|
const countDisplay = document.getElementById('seedCount');
|
|
|
|
let width, height;
|
|
let centerX, centerY;
|
|
let mouse = { x: -1000, y: -1000 };
|
|
let seeds = [];
|
|
const SEED_COUNT = 350;
|
|
let lastTime = 0;
|
|
let frameCount = 0;
|
|
let fps = 0;
|
|
let time = 0;
|
|
|
|
// 绒毛类
|
|
class Seed {
|
|
constructor() {
|
|
this.reset(true);
|
|
}
|
|
|
|
reset(initial = false) {
|
|
// 初始位置在蒲公英球体中心附近
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const radius = Math.random() * 60;
|
|
|
|
this.originX = centerX + Math.cos(angle) * radius * 0.5;
|
|
this.originY = centerY + Math.sin(angle) * radius * 0.3;
|
|
|
|
this.x = this.originX;
|
|
this.y = this.originY;
|
|
|
|
// 绒毛大小
|
|
this.size = 2 + Math.random() * 3;
|
|
|
|
// 绒毛伞状部分的半径
|
|
this.parachuteRadius = this.size * 3 + Math.random() * 4;
|
|
|
|
// 初始速度
|
|
this.vx = (Math.random() - 0.5) * 0.3;
|
|
this.vy = -Math.random() * 0.5 - 0.2;
|
|
|
|
// 绒毛柄的角度
|
|
this.angle = Math.random() * Math.PI * 2;
|
|
this.angleSpeed = (Math.random() - 0.5) * 0.02;
|
|
|
|
// 飘动参数
|
|
this.swayPhase = Math.random() * Math.PI * 2;
|
|
this.swaySpeed = 0.5 + Math.random() * 0.5;
|
|
this.swayAmplitude = 0.3 + Math.random() * 0.4;
|
|
|
|
// 状态
|
|
this.state = initial ? 'attached' : 'floating'; // attached, detaching, floating
|
|
this.detachTime = initial ? 5000 + Math.random() * 10000 : 0;
|
|
this.life = 0;
|
|
|
|
// 旋转
|
|
this.rotation = Math.random() * Math.PI * 2;
|
|
this.rotationSpeed = (Math.random() - 0.5) * 0.03;
|
|
|
|
// 上升或飘远
|
|
this.moveType = Math.random() > 0.4 ? 'rise' : 'drift';
|
|
|
|
// 透明度
|
|
this.alpha = 0.7 + Math.random() * 0.3;
|
|
}
|
|
|
|
update(dt, windX, windY) {
|
|
this.life += dt;
|
|
|
|
// 附着在球体上的状态
|
|
if (this.state === 'attached') {
|
|
// 轻微摆动
|
|
this.x = this.originX + Math.sin(time * this.swaySpeed + this.swayPhase) * 2;
|
|
this.y = this.originY + Math.cos(time * this.swaySpeed * 0.7 + this.swayPhase) * 1;
|
|
|
|
// 旋转
|
|
this.rotation += this.rotationSpeed;
|
|
|
|
// 检查是否应该脱离
|
|
if (this.life > this.detachTime) {
|
|
this.state = 'detaching';
|
|
}
|
|
|
|
// 鼠标靠近也会让绒毛脱离
|
|
const dx = mouse.x - this.x;
|
|
const dy = mouse.y - this.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < 80) {
|
|
this.state = 'detaching';
|
|
this.vx += (dx / dist) * 2;
|
|
this.vy += (dy / dist) * 2;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 脱离瞬间
|
|
if (this.state === 'detaching') {
|
|
this.state = 'floating';
|
|
// 给一个初始的飘散速度
|
|
const escapeAngle = Math.atan2(this.y - centerY, this.x - centerX);
|
|
this.vx += Math.cos(escapeAngle) * 0.5;
|
|
this.vy += Math.sin(escapeAngle) * 0.3 - 0.5;
|
|
}
|
|
|
|
// 飘浮状态
|
|
// 风力影响 (正弦波模拟)
|
|
const windEffect = Math.sin(time * 0.5 + this.swayPhase) * this.swayAmplitude;
|
|
|
|
// 鼠标排斥力
|
|
let repelX = 0, repelY = 0;
|
|
const dx = this.x - mouse.x;
|
|
const dy = this.y - mouse.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist < 120 && dist > 0) {
|
|
const force = (120 - dist) / 120;
|
|
repelX = (dx / dist) * force * 3;
|
|
repelY = (dy / dist) * force * 3;
|
|
}
|
|
|
|
// 应用速度
|
|
this.vx += windX * 0.01 + windEffect * 0.01 + repelX * 0.1;
|
|
this.vy += windY * 0.01 + repelY * 0.1;
|
|
|
|
// 根据移动类型调整速度
|
|
if (this.moveType === 'rise') {
|
|
this.vy -= 0.01; // 持续上升
|
|
} else {
|
|
this.vx += 0.005; // 向右飘远
|
|
}
|
|
|
|
// 阻力
|
|
this.vx *= 0.995;
|
|
this.vy *= 0.995;
|
|
|
|
// 位置更新
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
|
|
// 飘动效果
|
|
this.x += Math.sin(time * this.swaySpeed + this.swayPhase) * 0.3;
|
|
|
|
// 旋转
|
|
this.rotation += this.rotationSpeed + this.vx * 0.01;
|
|
|
|
// 检查是否需要重置
|
|
if (this.x > width + 50 || this.x < -50 ||
|
|
this.y < -50 || this.y > height + 50 ||
|
|
this.alpha <= 0) {
|
|
this.reset();
|
|
}
|
|
|
|
// 远处变透明
|
|
if (this.y < height * 0.3) {
|
|
this.alpha = Math.max(0.2, this.alpha - 0.002);
|
|
}
|
|
}
|
|
|
|
draw() {
|
|
ctx.save();
|
|
ctx.globalAlpha = this.alpha;
|
|
ctx.translate(this.x, this.y);
|
|
ctx.rotate(this.rotation);
|
|
|
|
if (this.state === 'attached') {
|
|
// 附着在球体上的绒毛 - 绘制简单的点状
|
|
this.drawAttached();
|
|
} else {
|
|
// 飘浮的绒毛 - 绘制完整形态
|
|
this.drawFloating();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawAttached() {
|
|
// 绒毛头部
|
|
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size);
|
|
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
|
|
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.6)');
|
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 细柄
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
|
ctx.lineWidth = 0.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(Math.cos(this.angle) * this.size * 2, Math.sin(this.angle) * this.size * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
drawFloating() {
|
|
// 绘制绒毛伞状部分(类似蒲公英种子)
|
|
const r = this.parachuteRadius;
|
|
|
|
// 伞状绒毛主体
|
|
const gradient = ctx.createRadialGradient(0, -r*0.3, 0, 0, -r*0.3, r);
|
|
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.95)');
|
|
gradient.addColorStop(0.3, 'rgba(255, 255, 255, 0.7)');
|
|
gradient.addColorStop(0.7, 'rgba(255, 255, 255, 0.3)');
|
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(0, -r*0.3, r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 绒毛放射状纹理
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
|
|
ctx.lineWidth = 0.3;
|
|
for (let i = 0; i < 8; i++) {
|
|
const angle = (Math.PI * 2 / 8) * i + this.rotation * 0.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(Math.cos(angle) * r * 0.8, Math.sin(angle) * r * 0.8 - r * 0.3);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// 种子柄
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
|
ctx.lineWidth = 0.8;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(0, r * 0.5);
|
|
ctx.stroke();
|
|
|
|
// 种子头
|
|
ctx.fillStyle = 'rgba(100, 80, 60, 0.8)';
|
|
ctx.beginPath();
|
|
ctx.arc(0, r * 0.5, this.size * 0.4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// 绘制树木剪影
|
|
function drawTrees() {
|
|
ctx.save();
|
|
|
|
// 模糊效果
|
|
ctx.filter = 'blur(2px)';
|
|
|
|
// 左侧树木群
|
|
drawTreeCluster(0, height, 0.8);
|
|
// 右侧树木群
|
|
drawTreeCluster(width, height, 0.7);
|
|
// 中间远处树木
|
|
drawTreeCluster(width * 0.3, height, 0.5);
|
|
drawTreeCluster(width * 0.7, height, 0.45);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawTreeCluster(x, y, scale) {
|
|
const treeCount = 3 + Math.floor(Math.random() * 3);
|
|
|
|
ctx.fillStyle = 'rgba(10, 15, 30, 0.7)';
|
|
|
|
for (let i = 0; i < treeCount; i++) {
|
|
const offsetX = (Math.random() - 0.5) * 150 * scale;
|
|
const treeHeight = (100 + Math.random() * 80) * scale;
|
|
const treeWidth = (40 + Math.random() * 30) * scale;
|
|
|
|
// 树干
|
|
ctx.fillRect(x + offsetX - treeWidth/4, y - treeHeight * 0.3, treeWidth/2, treeHeight * 0.3);
|
|
|
|
// 树冠(多层圆形)
|
|
ctx.beginPath();
|
|
ctx.arc(x + offsetX, y - treeHeight * 0.4, treeWidth, 0, Math.PI * 2);
|
|
ctx.arc(x + offsetX - treeWidth * 0.4, y - treeHeight * 0.3, treeWidth * 0.7, 0, Math.PI * 2);
|
|
ctx.arc(x + offsetX + treeWidth * 0.4, y - treeHeight * 0.3, treeWidth * 0.7, 0, Math.PI * 2);
|
|
ctx.arc(x + offsetX, y - treeHeight * 0.7, treeWidth * 0.8, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// 绘制蒲公英球体
|
|
function drawDandelionHead() {
|
|
// 主体光晕
|
|
const glowRadius = 80;
|
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, glowRadius);
|
|
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.3)');
|
|
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.15)');
|
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, glowRadius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 中心核心
|
|
ctx.fillStyle = 'rgba(200, 200, 220, 0.8)';
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, 15, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 核心细节纹理
|
|
ctx.fillStyle = 'rgba(150, 150, 170, 0.5)';
|
|
for (let i = 0; i < 20; i++) {
|
|
const angle = (Math.PI * 2 / 20) * i;
|
|
const r = 5 + Math.random() * 8;
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
centerX + Math.cos(angle) * r,
|
|
centerY + Math.sin(angle) * r,
|
|
2,
|
|
0, Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// 绘制星星
|
|
function drawStars() {
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
|
|
// 固定位置的星星(基于时间偏移)
|
|
for (let i = 0; i < 50; i++) {
|
|
const x = ((i * 137.5) % width);
|
|
const y = ((i * 89.7) % (height * 0.6));
|
|
const size = 0.5 + (i % 3) * 0.5;
|
|
const twinkle = Math.sin(time * 2 + i) * 0.3 + 0.7;
|
|
|
|
ctx.globalAlpha = twinkle * 0.6;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// 初始化
|
|
function init() {
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
window.addEventListener('mousemove', e => {
|
|
mouse.x = e.clientX;
|
|
mouse.y = e.clientY;
|
|
});
|
|
window.addEventListener('mouseleave', () => {
|
|
mouse.x = -1000;
|
|
mouse.y = -1000;
|
|
});
|
|
|
|
// 创建绒毛
|
|
for (let i = 0; i < SEED_COUNT; i++) {
|
|
seeds.push(new Seed());
|
|
}
|
|
|
|
countDisplay.textContent = SEED_COUNT;
|
|
}
|
|
|
|
function resize() {
|
|
width = canvas.width = window.innerWidth;
|
|
height = canvas.height = window.innerHeight;
|
|
centerX = width / 2;
|
|
centerY = height * 0.55;
|
|
|
|
// 重新设置所有绒毛的原点
|
|
seeds.forEach(seed => {
|
|
seed.originX = centerX + (seed.originX - seed.x) * 0.5;
|
|
seed.originY = centerY + (seed.originY - seed.y) * 0.5;
|
|
});
|
|
}
|
|
|
|
// 动画循环
|
|
function animate(currentTime) {
|
|
const dt = (currentTime - lastTime) / 1000;
|
|
lastTime = currentTime;
|
|
time = currentTime / 1000;
|
|
|
|
// 计算FPS
|
|
frameCount++;
|
|
if (frameCount % 30 === 0) {
|
|
fps = Math.round(1 / dt);
|
|
fpsDisplay.textContent = fps;
|
|
}
|
|
|
|
// 清空画布
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
// 绘制星星
|
|
drawStars();
|
|
|
|
// 绘制树木剪影
|
|
drawTrees();
|
|
|
|
// 计算风力(正弦波模拟)
|
|
const windX = Math.sin(time * 0.3) * 2 + Math.sin(time * 0.7) * 1;
|
|
const windY = Math.cos(time * 0.5) * 0.5;
|
|
|
|
// 绘制蒲公英球体
|
|
drawDandelionHead();
|
|
|
|
// 更新和绘制所有绒毛
|
|
seeds.forEach(seed => {
|
|
seed.update(dt * 1000, windX, windY);
|
|
seed.draw();
|
|
});
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
// 启动
|
|
init();
|
|
requestAnimationFrame(animate);
|
|
</script>
|
|
</body>
|
|
</html>
|