a
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user