469 lines
18 KiB
HTML
469 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>夏夜萤火虫 - Fireflies</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
overflow: hidden;
|
|
background: #000;
|
|
}
|
|
|
|
#canvas {
|
|
display: block;
|
|
}
|
|
|
|
#stats {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: #ccff00;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
text-shadow: 0 0 10px #ccff00, 0 0 20px #ccff00;
|
|
z-index: 100;
|
|
pointer-events: none;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
#stats span {
|
|
display: block;
|
|
}
|
|
|
|
.glow-text {
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.8; }
|
|
50% { opacity: 1; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="stats">
|
|
<span class="glow-text">FPS: <span id="fps">0</span></span>
|
|
<span class="glow-text">Fireflies: <span id="count">0</span></span>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
// 场景设置
|
|
const scene = new THREE.Scene();
|
|
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
|
|
scene.background = new THREE.Color(0x050510);
|
|
|
|
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.set(0, 5, 30);
|
|
camera.lookAt(0, 5, 0);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// 萤火虫配置
|
|
const FIREFLY_COUNT = 650;
|
|
const STATE_RESTING = 0;
|
|
const STATE_FLYING = 1;
|
|
|
|
// 创建萤火虫纹理
|
|
function createFireflyTexture() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 64;
|
|
canvas.height = 64;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 径向渐变 - 发光效果
|
|
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 30);
|
|
gradient.addColorStop(0, 'rgba(204, 255, 0, 1)');
|
|
gradient.addColorStop(0.3, 'rgba(170, 255, 0, 0.8)');
|
|
gradient.addColorStop(0.6, 'rgba(170, 255, 0, 0.3)');
|
|
gradient.addColorStop(1, 'rgba(170, 255, 0, 0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
const fireflyTexture = createFireflyTexture();
|
|
|
|
// 萤火虫材质
|
|
const fireflyMaterial = new THREE.SpriteMaterial({
|
|
map: fireflyTexture,
|
|
color: 0xccff00,
|
|
transparent: true,
|
|
opacity: 1,
|
|
blending: THREE.AdditiveBlending,
|
|
depthWrite: false
|
|
});
|
|
|
|
// 萤火虫类
|
|
class Firefly {
|
|
constructor() {
|
|
this.sprite = new THREE.Sprite(fireflyMaterial.clone());
|
|
this.reset();
|
|
scene.add(this.sprite);
|
|
}
|
|
|
|
reset() {
|
|
// 随机位置 - 分布在场景中
|
|
this.position = new THREE.Vector3(
|
|
(Math.random() - 0.5) * 60,
|
|
Math.random() * 15 + 0.5,
|
|
(Math.random() - 0.5) * 40 - 5
|
|
);
|
|
|
|
this.sprite.position.copy(this.position);
|
|
|
|
// 缩放 - 远处的小一点
|
|
const depthScale = 1 - (this.position.z + 25) / 50;
|
|
this.baseScale = 0.3 + Math.random() * 0.4 * depthScale;
|
|
this.sprite.scale.set(this.baseScale, this.baseScale, 1);
|
|
|
|
// 颜色变化
|
|
const colorChoice = Math.random() > 0.5 ? 0xccff00 : 0xaaff00;
|
|
this.sprite.material.color.setHex(colorChoice);
|
|
|
|
// 闪烁参数
|
|
this.blinkPhase = Math.random() * Math.PI * 2;
|
|
this.blinkSpeed = 0.5 + Math.random() * 1.5;
|
|
this.baseOpacity = 0.3 + Math.random() * 0.7;
|
|
|
|
// 状态
|
|
this.state = Math.random() > 0.3 ? STATE_RESTING : STATE_FLYING;
|
|
this.stateTimer = Math.random() * 5;
|
|
this.stateDuration = 3 + Math.random() * 6;
|
|
|
|
// 飞行参数
|
|
this.velocity = new THREE.Vector3(
|
|
(Math.random() - 0.5) * 0.02,
|
|
(Math.random() - 0.5) * 0.02,
|
|
(Math.random() - 0.5) * 0.01
|
|
);
|
|
|
|
// 噪声偏移
|
|
this.noiseOffset = new THREE.Vector3(
|
|
Math.random() * 100,
|
|
Math.random() * 100,
|
|
Math.random() * 100
|
|
);
|
|
|
|
// 停留目标位置
|
|
this.restTarget = this.position.clone();
|
|
this.findRestTarget();
|
|
}
|
|
|
|
findRestTarget() {
|
|
// 寻找附近的树枝/草丛位置作为停留点
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const radius = 5 + Math.random() * 15;
|
|
const height = Math.random() * 8;
|
|
|
|
this.restTarget.set(
|
|
Math.cos(angle) * radius + (Math.random() - 0.5) * 10,
|
|
height * 0.5 + 0.3,
|
|
Math.sin(angle) * radius * 0.5 - 5 + (Math.random() - 0.5) * 10
|
|
);
|
|
}
|
|
|
|
// 简单的噪声函数
|
|
noise(x, y, z, time) {
|
|
return Math.sin(x + time) * Math.cos(y + time * 0.7) * Math.sin(z + time * 0.3);
|
|
}
|
|
|
|
update(deltaTime, time) {
|
|
// 更新状态计时器
|
|
this.stateTimer += deltaTime;
|
|
|
|
// 状态切换
|
|
if (this.stateTimer > this.stateDuration) {
|
|
this.stateTimer = 0;
|
|
this.stateDuration = 2 + Math.random() * 8;
|
|
|
|
if (this.state === STATE_RESTING) {
|
|
this.state = STATE_FLYING;
|
|
this.findRestTarget(); // 找新的飞行目标
|
|
} else {
|
|
this.state = STATE_RESTING;
|
|
this.findRestTarget();
|
|
}
|
|
}
|
|
|
|
// 根据状态更新位置
|
|
if (this.state === STATE_FLYING) {
|
|
// 飞行状态 - 使用噪声产生自然飞舞
|
|
const noiseX = this.noise(this.noiseOffset.x, 0, 0, time) * 0.03;
|
|
const noiseY = this.noise(0, this.noiseOffset.y, 0, time) * 0.02;
|
|
const noiseZ = this.noise(0, 0, this.noiseOffset.z, time) * 0.015;
|
|
|
|
// 向目标飞行的倾向
|
|
const toTarget = new THREE.Vector3().subVectors(this.restTarget, this.position);
|
|
toTarget.multiplyScalar(0.005);
|
|
|
|
this.velocity.x += noiseX + toTarget.x * deltaTime;
|
|
this.velocity.y += noiseY + toTarget.y * deltaTime;
|
|
this.velocity.z += noiseZ + toTarget.z * deltaTime;
|
|
|
|
// 阻尼
|
|
this.velocity.multiplyScalar(0.98);
|
|
|
|
// 限制速度
|
|
this.velocity.clampLength(0, 0.08);
|
|
|
|
this.position.add(this.velocity);
|
|
|
|
// 边界限制
|
|
this.position.x = THREE.MathUtils.clamp(this.position.x, -30, 30);
|
|
this.position.y = THREE.MathUtils.clamp(this.position.y, 0.5, 18);
|
|
this.position.z = THREE.MathUtils.clamp(this.position.z, -25, 15);
|
|
} else {
|
|
// 停留状态 - 几乎不动,轻微漂浮
|
|
const floatX = Math.sin(time * 2 + this.blinkPhase) * 0.002;
|
|
const floatY = Math.cos(time * 1.5 + this.blinkPhase) * 0.003;
|
|
|
|
this.position.x += floatX;
|
|
this.position.y += floatY;
|
|
|
|
// 轻柔地趋向停留点
|
|
const toRest = new THREE.Vector3().subVectors(this.restTarget, this.position);
|
|
toRest.multiplyScalar(0.01);
|
|
this.position.add(toRest);
|
|
}
|
|
|
|
this.sprite.position.copy(this.position);
|
|
|
|
// 闪烁效果 - 使用透明度
|
|
const blink = Math.sin(time * this.blinkSpeed + this.blinkPhase);
|
|
let opacity = this.baseOpacity * (0.4 + 0.6 * (blink * 0.5 + 0.5));
|
|
|
|
// 飞行时闪烁更快
|
|
if (this.state === STATE_FLYING) {
|
|
opacity *= 0.8 + 0.4 * Math.sin(time * 3 + this.blinkPhase);
|
|
}
|
|
|
|
this.sprite.material.opacity = THREE.MathUtils.clamp(opacity, 0.1, 1);
|
|
|
|
// 根据透明度微调大小(闪烁时稍微变大)
|
|
const scalePulse = 1 + opacity * 0.2;
|
|
this.sprite.scale.set(
|
|
this.baseScale * scalePulse,
|
|
this.baseScale * scalePulse,
|
|
1
|
|
);
|
|
|
|
// 更新噪声偏移
|
|
this.noiseOffset.x += deltaTime * 0.5;
|
|
this.noiseOffset.y += deltaTime * 0.3;
|
|
this.noiseOffset.z += deltaTime * 0.2;
|
|
}
|
|
}
|
|
|
|
// 创建萤火虫群
|
|
const fireflies = [];
|
|
for (let i = 0; i < FIREFLY_COUNT; i++) {
|
|
fireflies.push(new Firefly());
|
|
}
|
|
|
|
// 创建背景树木剪影
|
|
function createTreeSilhouettes() {
|
|
const treeGroup = new THREE.Group();
|
|
|
|
// 创建树的形状
|
|
function createTree(x, z, scale) {
|
|
const treeCanvas = document.createElement('canvas');
|
|
treeCanvas.width = 128;
|
|
treeCanvas.height = 256;
|
|
const ctx = treeCanvas.getContext('2d');
|
|
|
|
// 渐变深色
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, 256);
|
|
gradient.addColorStop(0, '#0a1520');
|
|
gradient.addColorStop(0.5, '#0d1a25');
|
|
gradient.addColorStop(1, '#051015');
|
|
|
|
ctx.fillStyle = gradient;
|
|
|
|
// 绘制树冠轮廓(不规则形状)
|
|
ctx.beginPath();
|
|
ctx.moveTo(64, 20);
|
|
ctx.lineTo(100, 80);
|
|
ctx.lineTo(110, 140);
|
|
ctx.lineTo(90, 180);
|
|
ctx.lineTo(100, 220);
|
|
ctx.lineTo(70, 240);
|
|
ctx.lineTo(30, 220);
|
|
ctx.lineTo(40, 180);
|
|
ctx.lineTo(20, 140);
|
|
ctx.lineTo(30, 80);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// 树干
|
|
ctx.fillStyle = '#030508';
|
|
ctx.fillRect(54, 220, 20, 36);
|
|
|
|
const texture = new THREE.CanvasTexture(treeCanvas);
|
|
const material = new THREE.SpriteMaterial({
|
|
map: texture,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
color: 0x333344
|
|
});
|
|
|
|
const sprite = new THREE.Sprite(material);
|
|
sprite.position.set(x, 8 * scale, z);
|
|
sprite.scale.set(6 * scale, 12 * scale, 1);
|
|
return sprite;
|
|
}
|
|
|
|
// 创建多棵树
|
|
const treePositions = [
|
|
[-25, -20, 1.5], [-15, -25, 1.2], [-5, -22, 1.8],
|
|
[8, -28, 1.3], [20, -24, 1.6], [28, -18, 1.1],
|
|
[-30, -10, 1.4], [32, -12, 1.3], [-22, 5, 1.0],
|
|
[25, 8, 1.2], [-10, -5, 0.9], [12, -8, 1.1],
|
|
[-35, 2, 1.3], [35, -5, 1.2]
|
|
];
|
|
|
|
treePositions.forEach(pos => {
|
|
const tree = createTree(pos[0], pos[1], pos[2]);
|
|
treeGroup.add(tree);
|
|
});
|
|
|
|
// 添加地面雾层
|
|
const groundCanvas = document.createElement('canvas');
|
|
groundCanvas.width = 256;
|
|
groundCanvas.height = 64;
|
|
const gctx = groundCanvas.getContext('2d');
|
|
const ggradient = gctx.createLinearGradient(0, 0, 0, 64);
|
|
ggradient.addColorStop(0, 'rgba(5, 10, 15, 0)');
|
|
ggradient.addColorStop(0.5, 'rgba(8, 15, 22, 0.6)');
|
|
ggradient.addColorStop(1, 'rgba(10, 20, 30, 0.95)');
|
|
gctx.fillStyle = ggradient;
|
|
gctx.fillRect(0, 0, 256, 64);
|
|
|
|
const groundTexture = new THREE.CanvasTexture(groundCanvas);
|
|
const groundMaterial = new THREE.SpriteMaterial({
|
|
map: groundTexture,
|
|
transparent: true
|
|
});
|
|
const ground = new THREE.Sprite(groundMaterial);
|
|
ground.position.set(0, -2, -10);
|
|
ground.scale.set(80, 15, 1);
|
|
treeGroup.add(ground);
|
|
|
|
return treeGroup;
|
|
}
|
|
|
|
const trees = createTreeSilhouettes();
|
|
scene.add(trees);
|
|
|
|
// 添加星空背景
|
|
function createStars() {
|
|
const starGeometry = new THREE.BufferGeometry();
|
|
const starCount = 200;
|
|
const positions = new Float32Array(starCount * 3);
|
|
const sizes = new Float32Array(starCount);
|
|
|
|
for (let i = 0; i < starCount; i++) {
|
|
positions[i * 3] = (Math.random() - 0.5) * 100;
|
|
positions[i * 3 + 1] = 15 + Math.random() * 30;
|
|
positions[i * 3 + 2] = -30 - Math.random() * 20;
|
|
sizes[i] = Math.random() * 2;
|
|
}
|
|
|
|
starGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
starGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|
|
|
const starMaterial = new THREE.PointsMaterial({
|
|
color: 0x88aaff,
|
|
size: 0.3,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
sizeAttenuation: true
|
|
});
|
|
|
|
return new THREE.Points(starGeometry, starMaterial);
|
|
}
|
|
|
|
const stars = createStars();
|
|
scene.add(stars);
|
|
|
|
// 添加月光效果
|
|
const moonLight = new THREE.DirectionalLight(0x445577, 0.3);
|
|
moonLight.position.set(20, 40, -20);
|
|
scene.add(moonLight);
|
|
|
|
// 环境光
|
|
const ambientLight = new THREE.AmbientLight(0x0a1525, 0.4);
|
|
scene.add(ambientLight);
|
|
|
|
// 统计数据
|
|
let lastTime = 0;
|
|
let frameCount = 0;
|
|
let fps = 0;
|
|
const fpsElement = document.getElementById('fps');
|
|
const countElement = document.getElementById('count');
|
|
countElement.textContent = FIREFLY_COUNT;
|
|
|
|
// 动画循环
|
|
function animate(currentTime) {
|
|
requestAnimationFrame(animate);
|
|
|
|
const deltaTime = (currentTime - lastTime) / 1000;
|
|
lastTime = currentTime;
|
|
|
|
// 计算FPS
|
|
frameCount++;
|
|
if (frameCount % 30 === 0) {
|
|
fps = Math.round(1000 / ((currentTime - (animate.lastFpsTime || currentTime)) / 30));
|
|
animate.lastFpsTime = currentTime;
|
|
fpsElement.textContent = fps;
|
|
}
|
|
|
|
const time = currentTime / 1000;
|
|
|
|
// 更新所有萤火虫
|
|
fireflies.forEach(firefly => {
|
|
firefly.update(0.016, time);
|
|
});
|
|
|
|
// 缓慢旋转星空
|
|
stars.rotation.y = time * 0.005;
|
|
|
|
// 轻微相机移动
|
|
camera.position.x = Math.sin(time * 0.1) * 2;
|
|
camera.position.y = 5 + Math.cos(time * 0.08) * 1;
|
|
camera.lookAt(0, 6, 0);
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// 窗口大小调整
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
// 鼠标交互 - 萤火虫轻微躲避
|
|
let mouseX = 0, mouseY = 0;
|
|
window.addEventListener('mousemove', (e) => {
|
|
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
|
|
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
|
|
});
|
|
|
|
animate(0);
|
|
</script>
|
|
</body>
|
|
</html>
|