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

614 lines
22 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>孔明灯 · 许愿升空</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: linear-gradient(to bottom, #0a0a1a 0%, #1a1a3e 100%);
}
#canvas-container {
width: 100vw;
height: 100vh;
position: relative;
}
.ui-panel {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
z-index: 100;
background: rgba(10, 10, 26, 0.8);
padding: 15px 25px;
border-radius: 30px;
border: 1px solid rgba(220, 20, 60, 0.3);
backdrop-filter: blur(10px);
}
.btn {
padding: 12px 24px;
background: linear-gradient(135deg, #dc143c 0%, #b01030 100%);
color: #ffd700;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
font-family: "Microsoft YaHei", "SimHei", sans-serif;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(220, 20, 60, 0.4);
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(220, 20, 60, 0.6);
background: linear-gradient(135deg, #e0254d 0%, #c02040 100%);
}
.btn:active {
transform: translateY(0);
}
.info {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: #ffd700;
font-family: "Microsoft YaHei", "SimHei", sans-serif;
font-size: 16px;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
z-index: 100;
pointer-events: none;
}
.lantern-count {
position: absolute;
top: 50px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 215, 0, 0.8);
font-family: "Microsoft YaHei", sans-serif;
font-size: 14px;
z-index: 100;
pointer-events: none;
}
.wish-text {
position: absolute;
color: #ffd700;
font-family: "Microsoft YaHei", "SimHei", sans-serif;
font-size: 18px;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.8);
pointer-events: none;
opacity: 0;
animation: wishFloat 3s ease-out forwards;
}
@keyframes wishFloat {
0% {
opacity: 0;
transform: translateY(0) scale(0.8);
}
20% {
opacity: 1;
transform: translateY(-30px) scale(1);
}
80% {
opacity: 0.8;
transform: translateY(-80px) scale(1);
}
100% {
opacity: 0;
transform: translateY(-120px) scale(0.9);
}
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<div class="info">✨ 点击屏幕任意位置放飞孔明灯 ✨</div>
<div class="lantern-count" id="lanternCount">当前孔明灯: 0</div>
<div class="ui-panel">
<button class="btn" onclick="releaseBatch()">🎐 批量放飞 (5个)</button>
<button class="btn" onclick="releaseMany()">🏮 同时放飞 (20个)</button>
<button class="btn" onclick="autoReleaseToggle()" id="autoBtn">▶️ 自动放飞</button>
</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.002);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 60);
camera.lookAt(0, 30, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x000000, 0);
document.getElementById('canvas-container').appendChild(renderer.domElement);
// 环境光
const ambientLight = new THREE.AmbientLight(0x404080, 0.5);
scene.add(ambientLight);
// 月光
const moonLight = new THREE.DirectionalLight(0xaaccff, 0.3);
moonLight.position.set(50, 100, 50);
scene.add(moonLight);
// 灯笼数组
const lanterns = [];
const wishes = [
'心想事成', '万事如意', '平安喜乐', '阖家幸福', '前程似锦',
'身体健康', '学业有成', '事业有成', '财源广进', '爱情甜蜜',
'岁岁平安', '步步高升', '龙马精神', '吉祥如意', '五福临门'
];
// 创建灯笼纹理 - 使用Canvas绘制
function createLanternTexture() {
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 192;
const ctx = canvas.getContext('2d');
// 灯笼主体 - 椭圆形状
const gradient = ctx.createRadialGradient(64, 96, 10, 64, 96, 80);
gradient.addColorStop(0, '#ff6b6b');
gradient.addColorStop(0.3, '#dc143c');
gradient.addColorStop(0.7, '#a01030');
gradient.addColorStop(1, '#600820');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.ellipse(64, 96, 50, 80, 0, 0, Math.PI * 2);
ctx.fill();
// 顶部装饰
ctx.fillStyle = '#8b0000';
ctx.beginPath();
ctx.ellipse(64, 20, 30, 10, 0, 0, Math.PI * 2);
ctx.fill();
// 底部装饰
ctx.beginPath();
ctx.ellipse(64, 172, 35, 12, 0, 0, Math.PI * 2);
ctx.fill();
// 灯笼骨架线条
ctx.strokeStyle = 'rgba(139, 0, 0, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(64, 20);
ctx.lineTo(64, 172);
ctx.stroke();
// 横向骨架
for (let y = 50; y < 160; y += 35) {
ctx.beginPath();
ctx.ellipse(64, y, 48 * Math.sin(Math.acos(Math.abs(y - 96) / 80)), 3, 0, 0, Math.PI * 2);
ctx.stroke();
}
// 金色装饰边框
ctx.strokeStyle = '#ffd700';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(64, 20, 30, 10, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.ellipse(64, 172, 35, 12, 0, 0, Math.PI * 2);
ctx.stroke();
const texture = new THREE.CanvasTexture(canvas);
return texture;
}
// 创建火焰粒子纹理
function createFireTexture() {
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, 32);
gradient.addColorStop(0, 'rgba(255, 255, 200, 1)');
gradient.addColorStop(0.2, 'rgba(255, 215, 0, 0.9)');
gradient.addColorStop(0.5, 'rgba(255, 102, 0, 0.6)');
gradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);
return new THREE.CanvasTexture(canvas);
}
// 创建星星背景
function createStars() {
const starGeometry = new THREE.BufferGeometry();
const starCount = 300;
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) * 400;
positions[i * 3 + 1] = Math.random() * 200 + 50;
positions[i * 3 + 2] = (Math.random() - 0.5) * 200 - 50;
sizes[i] = Math.random() * 2 + 0.5;
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
starGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const starMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: 1.5,
transparent: true,
opacity: 0.8,
sizeAttenuation: true
});
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
return stars;
}
// 创建城市剪影
function createCitySilhouette() {
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 256;
const ctx = canvas.getContext('2d');
// 绘制远山轮廓
ctx.fillStyle = '#050510';
ctx.beginPath();
ctx.moveTo(0, 256);
for (let x = 0; x <= 1024; x += 20) {
const height = 80 + Math.sin(x * 0.01) * 40 + Math.cos(x * 0.03) * 30 + Math.random() * 20;
ctx.lineTo(x, 256 - height);
}
ctx.lineTo(1024, 256);
ctx.closePath();
ctx.fill();
// 绘制城市建筑剪影
ctx.fillStyle = '#080818';
let x = 0;
while (x < 1024) {
const width = 15 + Math.random() * 40;
const height = 30 + Math.random() * 100;
ctx.fillRect(x, 256 - height, width, height);
// 窗户灯光
if (Math.random() > 0.5) {
ctx.fillStyle = '#ffd700';
for (let wy = 256 - height + 10; wy < 250; wy += 15) {
if (Math.random() > 0.7) {
ctx.fillRect(x + 3, wy, 4, 6);
}
if (Math.random() > 0.7) {
ctx.fillRect(x + width - 8, wy, 4, 6);
}
}
ctx.fillStyle = '#080818';
}
x += width + 2;
}
const texture = new THREE.CanvasTexture(canvas);
const geometry = new THREE.PlaneGeometry(300, 75);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.9
});
const city = new THREE.Mesh(geometry, material);
city.position.set(0, 0, -30);
scene.add(city);
return city;
}
// 灯笼类
class Lantern {
constructor(x, z, isBatch = false) {
this.group = new THREE.Group();
// 灯笼主体 - 使用Sprite
const lanternTexture = createLanternTexture();
const lanternMaterial = new THREE.SpriteMaterial({
map: lanternTexture,
transparent: true,
opacity: 0.95
});
this.lantern = new THREE.Sprite(lanternMaterial);
this.lantern.scale.set(6, 9, 1);
this.group.add(this.lantern);
// 火焰粒子系统
this.createFireParticles();
// 火焰光源
this.light = new THREE.PointLight(0xff6600, 1, 20);
this.light.position.set(0, -2, 1);
this.group.add(this.light);
// 初始位置
this.group.position.set(x, isBatch ? 5 + Math.random() * 10 : 0, z);
// 运动参数
this.speed = 0.08 + Math.random() * 0.06;
this.swayAmplitude = 0.3 + Math.random() * 0.4;
this.swayFrequency = 0.5 + Math.random() * 0.5;
this.swayOffset = Math.random() * Math.PI * 2;
this.initialX = x;
this.initialZ = z;
this.time = 0;
this.opacity = 1;
// 最大高度
this.maxHeight = 80 + Math.random() * 40;
scene.add(this.group);
}
createFireParticles() {
const particleCount = 8;
const fireTexture = createFireTexture();
for (let i = 0; i < particleCount; i++) {
const material = new THREE.SpriteMaterial({
map: fireTexture,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
const particle = new THREE.Sprite(material);
particle.scale.set(1.5 + Math.random(), 1.5 + Math.random(), 1);
particle.position.set(
(Math.random() - 0.5) * 1.5,
-3 - Math.random() * 1.5,
0.5 + Math.random() * 0.5
);
particle.userData = {
initialY: particle.position.y,
speed: 0.02 + Math.random() * 0.03,
offset: Math.random() * Math.PI * 2,
scaleBase: particle.scale.x
};
this.group.add(particle);
}
this.fireParticles = this.group.children.filter(child => child !== this.lantern && child !== this.light);
}
update(deltaTime) {
this.time += deltaTime;
// 上升
this.group.position.y += this.speed;
// 左右摇摆(风力)
const sway = Math.sin(this.time * this.swayFrequency + this.swayOffset) * this.swayAmplitude;
this.group.position.x = this.initialX + sway;
this.group.position.z = this.initialZ + Math.cos(this.time * this.swayFrequency * 0.7) * (this.swayAmplitude * 0.5);
// 火焰闪烁效果
this.fireParticles.forEach((particle, i) => {
const flicker = Math.sin(this.time * 8 + particle.userData.offset) * 0.3 + 0.7;
particle.material.opacity = flicker * 0.8;
particle.scale.setScalar(particle.userData.scaleBase * (0.8 + flicker * 0.4));
// 粒子轻微跳动
particle.position.y = particle.userData.initialY + Math.sin(this.time * 10 + i) * 0.3;
});
// 光源闪烁
this.light.intensity = 0.8 + Math.sin(this.time * 6) * 0.3;
this.light.color.setHSL(0.08 + Math.sin(this.time * 4) * 0.02, 1, 0.5);
// 达到一定高度后淡出
if (this.group.position.y > this.maxHeight - 20) {
this.opacity = Math.max(0, 1 - (this.group.position.y - (this.maxHeight - 20)) / 20);
this.lantern.material.opacity = this.opacity * 0.95;
this.fireParticles.forEach(p => p.material.opacity *= this.opacity);
this.light.intensity *= this.opacity;
}
// 达到最大高度返回true表示需要移除
return this.group.position.y >= this.maxHeight;
}
dispose() {
scene.remove(this.group);
this.lantern.material.map.dispose();
this.lantern.material.dispose();
this.fireParticles.forEach(p => {
p.material.map.dispose();
p.material.dispose();
});
}
}
// 创建灯笼
function createLantern(x, z, isBatch = false) {
const lantern = new Lantern(x, z, isBatch);
lanterns.push(lantern);
updateCount();
return lantern;
}
// 批量释放
function releaseBatch() {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const x = (Math.random() - 0.5) * 80;
const z = (Math.random() - 0.5) * 40 - 10;
createLantern(x, z, true);
showWish();
}, i * 300);
}
}
// 大量释放
function releaseMany() {
for (let i = 0; i < 20; i++) {
setTimeout(() => {
const x = (Math.random() - 0.5) * 100;
const z = (Math.random() - 0.5) * 50 - 10;
createLantern(x, z, true);
}, i * 100);
}
}
// 自动释放控制
let autoRelease = false;
let autoReleaseInterval;
function autoReleaseToggle() {
autoRelease = !autoRelease;
const btn = document.getElementById('autoBtn');
if (autoRelease) {
btn.textContent = '⏸️ 停止自动';
btn.style.background = 'linear-gradient(135deg, #666 0%, #444 100%)';
autoReleaseInterval = setInterval(() => {
if (lanterns.length < 50) {
const x = (Math.random() - 0.5) * 100;
const z = (Math.random() - 0.5) * 50 - 10;
createLantern(x, z, true);
}
}, 500);
} else {
btn.textContent = '▶️ 自动放飞';
btn.style.background = '';
clearInterval(autoReleaseInterval);
}
}
// 显示许愿文字
function showWish(x, y) {
const wish = wishes[Math.floor(Math.random() * wishes.length)];
const el = document.createElement('div');
el.className = 'wish-text';
el.textContent = '✨ ' + wish + ' ✨';
const screenX = x || window.innerWidth / 2 + (Math.random() - 0.5) * 200;
const screenY = y || window.innerHeight - 100;
el.style.left = screenX + 'px';
el.style.top = screenY + 'px';
document.body.appendChild(el);
setTimeout(() => el.remove(), 3000);
}
// 更新计数
function updateCount() {
document.getElementById('lanternCount').textContent = '当前孔明灯: ' + lanterns.length;
}
// 鼠标点击释放灯笼
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click', (event) => {
// 避免点击按钮时触发
if (event.target.tagName === 'BUTTON') return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 在点击位置的水平面创建灯笼
const targetZ = -10 + Math.random() * 20;
const vector = new THREE.Vector3(mouse.x, mouse.y, 0.5);
vector.unproject(camera);
const dir = vector.sub(camera.position).normalize();
const distance = (targetZ - camera.position.z) / dir.z;
const pos = camera.position.clone().add(dir.multiplyScalar(distance));
createLantern(pos.x, targetZ);
showWish(event.clientX, event.clientY);
});
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 初始化场景元素
createStars();
createCitySilhouette();
// 初始释放一些灯笼
for (let i = 0; i < 8; i++) {
setTimeout(() => {
const x = (Math.random() - 0.5) * 60;
const z = (Math.random() - 0.5) * 30 - 10;
createLantern(x, z, true);
}, i * 400);
}
// 动画循环
let lastTime = 0;
function animate(currentTime) {
requestAnimationFrame(animate);
const deltaTime = (currentTime - lastTime) * 0.001 || 0.016;
lastTime = currentTime;
// 更新所有灯笼
for (let i = lanterns.length - 1; i >= 0; i--) {
const lantern = lanterns[i];
const shouldRemove = lantern.update(deltaTime);
if (shouldRemove) {
lantern.dispose();
lanterns.splice(i, 1);
updateCount();
}
}
// 星星闪烁
scene.children.forEach(child => {
if (child instanceof THREE.Points) {
child.rotation.y += 0.0002;
}
});
renderer.render(scene, camera);
}
animate(0);
</script>
</body>
</html>