Files
nanocode/particle-lab/18-dandelion/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

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>