🌩️ 构建闹鬼的天气应用:一次进入 3D Web 开发的惊悚之旅

发布: (2025年12月4日 GMT+8 11:17)
5 min read
原文: Dev.to

Source: Dev.to

介绍

当雾气滚滚、闪电划破天际时,你就知道已经不在堪萨斯了。我想打造一个把氛围视觉与实用功能结合在一起的作品——一个带点恐怖感的天气应用。于是诞生了 Eerie Weather App,一个 3D 天气可视化项目,漂浮的闹鬼小屋会根据全球各城市的实时天气做出反应。下雨时天空变暗并出现雨滴,雷暴时会释放锯齿状闪电,每种天气都有专属的粒子系统和环境音效。

设计

配色方案

  • 背景: #1a1a2e(深蓝紫)——像暴风雨前的天空。
  • 强调色: #460809#f4320b(悬停时)——血红的高光。
  • 文字发光: 以脉动的红色文字阴影呈现。
h3 {
  color: #ff6b6b;
  text-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}

排版

“October Crow” 字体带有锯齿、手绘的感觉。

@font-face {
  font-family: "October Crow";
  src: url("/fonts/October Crow.ttf") format("truetype");
}
body {
  font-family: "October Crow", Arial, sans-serif;
}

自定义按钮

按钮使用内联 SVG 背景,看起来像风化的石板。

/* Example background image */
background-image: url('data:image/svg+xml,');

Three.js 初始化

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 10, 50);

雾效增加深度,营造“有什么潜伏”的氛围。

闹鬼小屋模型

loader.load("models/forest_house.glb", (gltf) => {
  house = gltf.scene;
  house.userData.centerY = -center.y - 2.5;
  scene.add(house);
});

// Animation loop
house.position.y = house.userData.centerY + Math.sin(time * 0.5) * 0.2;

小屋会轻微上下晃动,随天气变化而响应。

灯光

const ambientLight = new THREE.AmbientLight(0x9999cc, 0.4);      // Moonlit base
const directionalLight = new THREE.DirectionalLight(0xaaaadd, 0.3); // Soft moonlight
const rimLight = new THREE.DirectionalLight(0x6666aa, 0.2);          // Subtle backlight

闪电效果

function createLightningBolt(startX, startY, startZ, endX, endY, endZ) {
  const points = [];
  const segments = 8 + Math.floor(Math.random() * 6);

  for (let i = 0; i < segments; i++) {
    // generate jittered points between start and end
    const t = i / (segments - 1);
    const x = THREE.MathUtils.lerp(startX, endX, t) + (Math.random() - 0.5) * 0.5;
    const y = THREE.MathUtils.lerp(startY, endY, t) + (Math.random() - 0.5) * 0.5;
    const z = THREE.MathUtils.lerp(startZ, endZ, t) + (Math.random() - 0.5) * 0.5;
    points.push(new THREE.Vector3(x, y, z));
  }

  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
  const line = new THREE.Line(geometry, material);
  scene.add(line);

  // Fade out after a short duration
  setTimeout(() => scene.remove(line), 200);
}

天气数据集成

const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,weather_code...`;
const data = await fetch(url).then(r => r.json());

将 Open‑Meteo 天气代码映射到恐怖效果:

let condition;
if ([95, 96, 99].includes(weatherCode)) condition = "Thunderstorm";
else if ([61, 63, 65].includes(weatherCode)) condition = "Rain";
else if ([71, 73, 75].includes(weatherCode)) condition = "Snow";

用户界面

  • 搜索栏 – 输入任意城市,观看天气“活”起来。
  • 底部栏 – 快捷按钮:
    • 🏠 Home – 重置相机。
    • ℹ️ About – 打开包含应用信息和致谢的模态框。
document.getElementById("home-btn").addEventListener("click", () => {
  camera.position.copy(initialCameraPosition);
  controls.target.copy(initialCameraTarget);
  controls.update();
});

面板可以最小化:

function togglePanel(panelId) {
  const panel = document.getElementById(panelId);
  panel.classList.toggle("minimized");
}

故障排查

问题解决方案
Lightning bolts cause “Computed radius is NaN” errors在创建几何体前验证坐标:`if (isNaN(startX)
Rapid weather changes trigger play()/pause() conflicts跟踪当前播放的音效,只暂停其他音效:Object.values(weatherSounds).forEach(s => { if (s !== sound) { s.pause(); s.currentTime = 0; } });
City search overrides manual weather selection使用 manualOverride 标记,在显示新 API 数据前重置:manualOverride = false; displayWeatherInfo(data);

结论

Eerie Weather App 已上线。搜索你的城市,调高雷暴强度,让闪电照亮屏幕。无论你是想找 Three.js 灵感,还是想用最戏剧化的方式查看天气,这款应用都能满足你的需求。

🌩️ Launch the Eerie Weather App

Back to Blog

相关文章

阅读更多 »

模块模式

基本结构 模块模式是利用 IIFE 和闭包(Closure)的特性,模拟出一种“类”的概念,拥有公有(Public)和私有(Private)的成员与方法。这是 JavaScript 实现封装(Encapsulation)的经典方式。 代码示例:一个计数器模块(JavaScript) var CounterModule…

[Boost]

🚀 客户端 vs 服务器端 CORS:了解真实差异 Shanthi 的开发日志 • 12 月 4 日 标签:webdev, cors, javascript, node