🎮ArcadeLab

点火放孔明灯 · 模拟器

by HyperMaker20
478 lines17.6 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>点火放孔明灯 · 模拟器</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      font-family: 'Microsoft YaHei', sans-serif;
    }
    #info {
      position: absolute;
      top: 20px;
      left: 20px;
      color: rgba(255, 255, 255, 0.9);
      background: rgba(0, 0, 0, 0.5);
      padding: 12px 24px;
      border-radius: 40px;
      backdrop-filter: blur(8px);
      pointer-events: none;
      z-index: 10;
      box-shadow: 0 4px 15px rgba(0,0,0,0.3);
      border-left: 4px solid #ffaa00;
    }
    #info h1 {
      margin: 0;
      font-size: 1.2rem;
      font-weight: 400;
      letter-spacing: 2px;
    }
    #info p {
      margin: 5px 0 0;
      font-size: 0.8rem;
      opacity: 0.8;
    }
    #ignite-btn {
      position: absolute;
      bottom: 40px;
      left: 50%;
      transform: translateX(-50%);
      padding: 16px 48px;
      font-size: 1.4rem;
      font-weight: bold;
      color: #fff;
      background: linear-gradient(45deg, #ff6a00, #ee3e00);
      border: none;
      border-radius: 60px;
      cursor: pointer;
      z-index: 20;
      box-shadow: 0 0 25px rgba(255, 100, 0, 0.8), 0 10px 20px rgba(0,0,0,0.3);
      letter-spacing: 4px;
      transition: all 0.3s ease;
      border: 1px solid rgba(255, 255, 200, 0.6);
      text-shadow: 0 2px 5px rgba(0,0,0,0.5);
    }
    #ignite-btn:hover {
      transform: translateX(-50%) scale(1.05);
      box-shadow: 0 0 40px rgba(255, 150, 0, 1), 0 15px 25px rgba(0,0,0,0.4);
      background: linear-gradient(45deg, #ff7b1c, #ff4d00);
    }
    #ignite-btn:active {
      transform: translateX(-50%) scale(0.95);
    }
    #ignite-btn:disabled {
      opacity: 0.5;
      pointer-events: none;
      filter: grayscale(0.6);
      box-shadow: 0 0 10px rgba(255, 100, 0, 0.3);
    }
    #status {
      position: absolute;
      bottom: 120px;
      left: 50%;
      transform: translateX(-50%);
      color: #ffdd99;
      background: rgba(20, 20, 30, 0.7);
      padding: 8px 24px;
      border-radius: 30px;
      font-size: 0.95rem;
      letter-spacing: 2px;
      backdrop-filter: blur(5px);
      border: 1px solid rgba(255, 200, 100, 0.4);
      transition: opacity 0.5s;
      z-index: 15;
      pointer-events: none;
      white-space: nowrap;
    }
    .instruction {
      position: absolute;
      bottom: 20px;
      right: 20px;
      color: rgba(255, 255, 255, 0.4);
      font-size: 0.7rem;
      z-index: 5;
    }
  </style>
</head>
<body>
  <div id="info">
    <h1>✨ 孔明灯 · 许愿 ✨</h1>
    <p>点击下方按钮点火 · 让它升空</p>
  </div>
  <div id="status">🏮 等待点火...</div>
  <button id="ignite-btn">点 火</button>
  <div class="instruction">拖动视角观看 | 模拟效果</div>

  <!-- 引入 Three.js 和 OrbitControls -->
  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.128.0/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.128.0/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    // --- 初始化场景、相机、渲染器 ---
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a1a); // 深蓝色夜空
    // 添加一点渐变效果:用雾来模拟
    scene.fog = new THREE.FogExp2(0x0a0a1a, 0.002);

    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(8, 6, 15);
    camera.lookAt(0, 3, 0);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true; // 虽然用不上但无妨
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setPixelRatio(window.devicePixelRatio);
    document.body.appendChild(renderer.domElement);

    // --- 轨道控制器 ---
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.autoRotate = false;
    controls.enableZoom = true;
    controls.target.set(0, 3, 0);
    controls.maxPolarAngle = Math.PI / 2.2; // 限制角度,不要看到地底
    controls.minDistance = 5;
    controls.maxDistance = 30;

    // --- 灯光 ---
    // 环境光
    const ambientLight = new THREE.AmbientLight(0x404060);
    scene.add(ambientLight);

    // 主光源 - 偏蓝色的月光
    const moonLight = new THREE.DirectionalLight(0xaaccff, 0.8);
    moonLight.position.set(-10, 20, 10);
    scene.add(moonLight);

    // 辅助背光暖色,让孔明灯更有氛围
    const backLight = new THREE.PointLight(0xffaa55, 0.5, 40);
    backLight.position.set(5, 5, -10);
    scene.add(backLight);

    // 底部补光
    const fillLight = new THREE.PointLight(0x4466aa, 0.4);
    fillLight.position.set(0, 2, 8);
    scene.add(fillLight);

    // --- 创建星空背景效果(粒子系统)---
    const starsGeometry = new THREE.BufferGeometry();
    const starsCount = 2000;
    const starsPositions = new Float32Array(starsCount * 3);
    for (let i = 0; i < starsCount * 3; i += 3) {
      // 随机分布在很大的球壳内
      const r = 80 + Math.random() * 60;
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos((Math.random() * 2) - 1);
      starsPositions[i] = Math.sin(phi) * Math.cos(theta) * r;
      starsPositions[i+1] = Math.sin(phi) * Math.sin(theta) * r * 0.6 + 30; // 让星星偏上
      starsPositions[i+2] = Math.cos(phi) * r;
    }
    starsGeometry.setAttribute('position', new THREE.BufferAttribute(starsPositions, 3));
    const starsMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.25, transparent: true, opacity: 0.9 });
    const stars = new THREE.Points(starsGeometry, starsMaterial);
    scene.add(stars);

    // 再加一些小而亮的星星
    const brightStarsGeo = new THREE.BufferGeometry();
    const brightCount = 300;
    const brightPositions = new Float32Array(brightCount * 3);
    for (let i = 0; i < brightCount * 3; i += 3) {
      const r = 120 + Math.random() * 80;
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos((Math.random() * 2) - 1);
      brightPositions[i] = Math.sin(phi) * Math.cos(theta) * r;
      brightPositions[i+1] = Math.sin(phi) * Math.sin(theta) * r * 0.7 + 40;
      brightPositions[i+2] = Math.cos(phi) * r;
    }
    brightStarsGeo.setAttribute('position', new THREE.BufferAttribute(brightPositions, 3));
    const brightStarsMat = new THREE.PointsMaterial({ color: 0xffeedd, size: 0.4, transparent: true, opacity: 1 });
    const brightStars = new THREE.Points(brightStarsGeo, brightStarsMat);
    scene.add(brightStars);

    // --- 地面 (微弱的草地/土地效果,简单平面) ---
    const groundGeometry = new THREE.CircleGeometry(40, 32);
    const groundMaterial = new THREE.MeshStandardMaterial({
      color: 0x1a2a1a,
      roughness: 0.9,
      metalness: 0.1,
      emissive: new THREE.Color(0x0a1a0a),
      side: THREE.DoubleSide
    });
    const ground = new THREE.Mesh(groundGeometry, groundMaterial);
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -0.05;
    scene.add(ground);

    // 地面上的小点装饰(模拟草地/石子)
    const grassGeo = new THREE.BufferGeometry();
    const grassCount = 800;
    const grassPositions = new Float32Array(grassCount * 3);
    for (let i = 0; i < grassCount * 3; i += 3) {
      const angle = Math.random() * Math.PI * 2;
      const rad = 3 + Math.random() * 15;
      grassPositions[i] = Math.cos(angle) * rad;
      grassPositions[i+1] = 0.02;
      grassPositions[i+2] = Math.sin(angle) * rad;
    }
    grassGeo.setAttribute('position', new THREE.BufferAttribute(grassPositions, 3));
    const grassMat = new THREE.PointsMaterial({ color: 0x3a5a3a, size: 0.08 });
    const grass = new THREE.Points(grassGeo, grassMat);
    scene.add(grass);

    // --- 构建孔明灯 ---
    const lanternGroup = new THREE.Group();
    lanternGroup.position.set(0, 0.8, 0); // 初始在地面上

    // 1. 灯体(主体):使用圆柱体+圆锥组合成经典孔明灯形状,带半透明纸张质感
    const bodyMaterial = new THREE.MeshStandardMaterial({
      color: 0xffdd99,
      emissive: new THREE.Color(0xffaa33),
      emissiveIntensity: 0.1, // 未点燃时很微弱
      transparent: true,
      opacity: 0.85,
      side: THREE.DoubleSide,
      roughness: 0.4,
      metalness: 0.1
    });

    // 中间圆柱部分
    const cylinder = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 0.9, 1.6, 16), bodyMaterial);
    cylinder.position.y = 0.8;
    cylinder.castShadow = true;
    cylinder.receiveShadow = false;
    lanternGroup.add(cylinder);

    // 上部圆锥顶
    const topCone = new THREE.Mesh(new THREE.ConeGeometry(0.9, 0.8, 16), bodyMaterial);
    topCone.position.y = 1.6 + 0.4; // 圆柱顶部在1.6,锥高0.8,中心在1.6+0.4
    topCone.castShadow = true;
    lanternGroup.add(topCone);

    // 底部开口的环(竹圈)
    const ringMaterial = new THREE.MeshStandardMaterial({ color: 0xcc9966, roughness: 0.7, metalness: 0.2 });
    const ring = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.06, 8, 24), ringMaterial);
    ring.rotation.x = Math.PI / 2;
    ring.position.y = 0.0;
    lanternGroup.add(ring);

    // 上部装饰环
    const ring2 = new THREE.Mesh(new THREE.TorusGeometry(0.9, 0.05, 8, 24), ringMaterial);
    ring2.rotation.x = Math.PI / 2;
    ring2.position.y = 1.6;
    lanternGroup.add(ring2);

    // 2. 内部的火焰/热源(用于点燃后发光)
    const flameMaterial = new THREE.MeshStandardMaterial({
      color: 0xff6600,
      emissive: new THREE.Color(0xff4400),
      emissiveIntensity: 0.0, // 初始不发光
      transparent: true,
      opacity: 0.0
    });
    const flame = new THREE.Mesh(new THREE.SphereGeometry(0.4, 8, 8), flameMaterial);
    flame.position.y = 0.4;
    flame.scale.set(1, 0.8, 1);
    lanternGroup.add(flame);

    // 加一个小火苗的粒子效果(稍后动态控制)
    const fireParticlesGeo = new THREE.BufferGeometry();
    const particleCount = 30;
    const particlePositions = new Float32Array(particleCount * 3);
    for (let i = 0; i < particleCount * 3; i += 3) {
      particlePositions[i] = (Math.random() - 0.5) * 0.6;
      particlePositions[i+1] = Math.random() * 0.8 + 0.2;
      particlePositions[i+2] = (Math.random() - 0.5) * 0.6;
    }
    fireParticlesGeo.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
    const fireParticlesMat = new THREE.PointsMaterial({
      color: 0xffaa00,
      size: 0.1,
      transparent: true,
      opacity: 0,
      blending: THREE.AdditiveBlending
    });
    const fireParticles = new THREE.Points(fireParticlesGeo, fireParticlesMat);
    fireParticles.position.y = 0.4;
    lanternGroup.add(fireParticles);

    // 3. 点缀一些飘带/绳子 (简单装饰)
    const ropeMaterial = new THREE.LineBasicMaterial({ color: 0xccaa88 });
    for (let i = 0; i < 4; i++) {
      const angle = (i / 4) * Math.PI * 2;
      const x = Math.cos(angle) * 0.85;
      const z = Math.sin(angle) * 0.85;
      const points = [
        new THREE.Vector3(x, 0.0, z),
        new THREE.Vector3(x * 1.1, -0.4, z * 1.1)
      ];
      const geometry = new THREE.BufferGeometry().setFromPoints(points);
      const line = new THREE.Line(geometry, ropeMaterial);
      lanternGroup.add(line);
    }

    scene.add(lanternGroup);

    // 在灯下加一个微弱的点光源,点燃后增强
    const lanternLight = new THREE.PointLight(0xff6600, 0, 10);
    lanternLight.position.set(0, 0.8, 0);
    lanternGroup.add(lanternLight);

    // --- 状态管理 ---
    let isIgnited = false;      // 是否已经点火
    let isFlying = false;       // 是否在升空
    let flySpeed = 0.0;        // 升空速度
    let time = 0;              // 时间累积,用于动画

    // 获取按钮和状态显示元素
    const igniteBtn = document.getElementById('ignite-btn');
    const statusDiv = document.getElementById('status');

    // 点燃函数
    function ignite() {
      if (isIgnited) return;
      isIgnited = true;
      isFlying = true;
      flySpeed = 0.0; // 初始速度
      
      // 更新按钮和状态
      igniteBtn.disabled = true;
      igniteBtn.textContent = '🔥 已点燃';
      statusDiv.textContent = '🏮 孔明灯正在升空... 许个愿吧 ✨';
      
      // 改变材质发光效果
      bodyMaterial.emissiveIntensity = 0.8;
      bodyMaterial.opacity = 0.95;
      flameMaterial.emissiveIntensity = 1.5;
      flameMaterial.opacity = 0.9;
      
      // 粒子效果显现
      fireParticlesMat.opacity = 0.9;
      
      // 灯光加强
      lanternLight.intensity = 2.5;
      lanternLight.distance = 15;
      
      // 改变火焰颜色稍微亮一点
      flameMaterial.color.setHex(0xffaa00);
      flameMaterial.emissive.setHex(0xff8800);
    }

    igniteBtn.addEventListener('click', ignite);

    // 重置/重新开始功能?这里不需要,但可以允许刷新页面重新来。为了友好,加一个隐藏的重置(通过双击按钮?不,保持简单)

    // --- 动画循环 ---
    function animate() {
      requestAnimationFrame(animate);
      
      const delta = 0.016; // 近似60fps
      time += delta;

      // 1. 如果已经点燃,孔明灯上升
      if (isFlying) {
        // 加速上升:速度逐渐增加
        flySpeed += 0.002;
        // 限制最大速度
        if (flySpeed > 0.08) flySpeed = 0.08;
        
        // 向上移动
        lanternGroup.position.y += flySpeed;
        
        // 轻微左右飘动(模拟风)
        lanternGroup.position.x += Math.sin(time * 1.5) * 0.002;
        lanternGroup.position.z += Math.cos(time * 1.8) * 0.002;
        
        // 一点旋转晃动
        lanternGroup.rotation.z = Math.sin(time * 2) * 0.03;
        lanternGroup.rotation.x = Math.cos(time * 2.3) * 0.03;
        lanternGroup.rotation.y += 0.001;

        // 如果飞得太高,消失并重置?为了模拟器体验,让它一直飞,但可能飞出视野。不过相机可以跟着?不强制。
        // 如果高度超过25,稍微降低一点透明度或者让它继续(让用户还能看到)
        if (lanternGroup.position.y > 22) {
          // 保持可见,但可以慢慢淡出?为了不丢失,我们让相机可自由观看,不重置。
          // 但为了防止无限上升,设置一个上限,然后悬停?还是让它飞走再重置?干脆让它继续,用户想看就跟着。
          // 为了更好的体验,在高度超过25后,加速上升直至消失?但用户可能想一直看。我们让它超过30后停止上升,悬停在高空?不好,还是让它可以一直飞,但最大速度限制,同时相机可以跟随。
          // 为了演示方便,我们到30高度后就停止上升,让灯悬停在高空,但有点奇怪,还是让它继续,但用户想看可以调视角。
          // 这里我们保持继续上升,但为了不飞出太远,让速度在高度高时降低?不,保持原样。
        }
      }

      // 2. 火焰/粒子动态闪烁 (无论是否点燃,但点燃后更明显)
      if (isIgnited) {
        // 火焰跳动
        const scale = 1 + Math.sin(time * 20) * 0.15;
        flame.scale.set(scale, 0.8 * scale, scale);
        
        // 粒子闪烁
        fireParticlesMat.size = 0.12 + Math.sin(time * 30) * 0.04;
        
        // 灯光强度微变
        lanternLight.intensity = 2.2 + Math.sin(time * 25) * 0.8;
        
        // 灯体发光强度微变
        bodyMaterial.emissiveIntensity = 0.7 + Math.sin(time * 15) * 0.2;
      } else {
        // 未点燃时,只有极其微弱的呼吸效果(模拟等待)
        bodyMaterial.emissiveIntensity = 0.08 + Math.sin(time * 3) * 0.03;
      }

      // 3. 星星缓慢旋转(增加动感)
      stars.rotation.y += 0.0001;
      brightStars.rotation.y -= 0.00015;

      // 4. 更新控制器
      controls.update();

      // 5. 让相机轻微自动跟随上升的灯?不强制,但可以稍微调整 controls.target 跟随?
      // 为了视角更好,如果灯上升超过一定高度,让 controls.target 慢慢跟随灯的位置,但保持相机相对。
      // 这样用户不用手动调整也能看到灯。但为了自由性,还是让用户控制。这里可选:温和跟随。
      if (isFlying && lanternGroup.position.y > 5) {
        // 让目标点平滑跟随灯的位置(但保留y轴偏移,使视角舒适)
        const targetY = lanternGroup.position.y + 1.5;
        controls.target.y += (targetY - controls.target.y) * 0.02;
        // 同时让相机也稍微提升,但保持相对角度?可以稍微向上移动相机
        // 这里不强制移动相机位置,因为OrbitControls会处理
      } else if (!isFlying && lanternGroup.position.y <= 1) {
        controls.target.y += (3 - controls.target.y) * 0.05; // 重置目标高度
      }

      // 渲染
      renderer.render(scene, camera);
    }

    animate();

    // --- 窗口大小自适应 ---
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });

    // 一些小提示:初始状态显示
    setTimeout(() => {
      statusDiv.style.opacity = '1';
    }, 100);

    // 禁用按钮双击选中文本等
    igniteBtn.addEventListener('mousedown', (e) => e.preventDefault());

    // 添加一点初始化时的飘动(未点燃时的小晃动?不必要)
  </script>
</body>
</html>

Game Source: 点火放孔明灯 · 模拟器

Creator: HyperMaker20

Libraries: three

Complexity: complex (478 lines, 17.6 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-hypermaker20-muhq3cx5" to link back to the original. Then publish at arcadelab.ai/publish.