🌩️ Building a Haunted Weather App: A Spooky Journey into 3D Web Development
Source: Dev.to
Introduction
When the fog rolls in and lightning strikes, you know you’re not in Kansas anymore. I wanted to build something that combined atmospheric visuals with practical functionality—a weather app that feels spooky. The result is the Eerie Weather App, a 3D weather visualization where a haunted floating house reacts to real‑time conditions from cities around the world. Rain darkens the sky and adds droplets, thunderstorms unleash jagged lightning, and each weather type has its own particle system and ambient sound.
Design
Color Scheme
- Background:
#1a1a2e(deep blue‑purple) – like the sky before a storm. - Accent:
#460809→#f4320bon hover – a blood‑red highlight. - Text Glow: Ethereal red with pulsing text shadows.
h3 {
color: #ff6b6b;
text-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}
Typography
The “October Crow” font gives a jagged, hand‑drawn feel.
@font-face {
font-family: "October Crow";
src: url("/fonts/October Crow.ttf") format("truetype");
}
body {
font-family: "October Crow", Arial, sans-serif;
}
Custom Buttons
Buttons are styled with inline SVG backgrounds that look like weathered stone tablets.
/* Example background image */
background-image: url('data:image/svg+xml,');
Three.js Setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 10, 50);
The fog adds depth and a “something lurking” atmosphere.
Haunted House Model
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;
The house gently bobs up and down, reacting to the weather.
Lighting
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
Lightning Effect
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);
}
Weather Data Integration
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code...`;
const data = await fetch(url).then(r => r.json());
Mapping Open‑Meteo weather codes to spooky effects:
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";
User Interface
- Search bar – find any city and watch the weather come alive.
- Bottom bar – quick access buttons:
- 🏠 Home – resets camera.
- ℹ️ About – opens a modal with app info and credits.
document.getElementById("home-btn").addEventListener("click", () => {
camera.position.copy(initialCameraPosition);
controls.target.copy(initialCameraTarget);
controls.update();
});
Panels can be minimized:
function togglePanel(panelId) {
const panel = document.getElementById(panelId);
panel.classList.toggle("minimized");
}
Troubleshooting
| Problem | Solution |
|---|---|
| Lightning bolts cause “Computed radius is NaN” errors | Validate coordinates before creating geometry: `if (isNaN(startX) |
Rapid weather changes trigger play()/pause() conflicts | Track the current sound and pause only others: Object.values(weatherSounds).forEach(s => { if (s !== sound) { s.pause(); s.currentTime = 0; } }); |
| City search overrides manual weather selection | Use a manualOverride flag that is reset before displaying new API data: manualOverride = false; displayWeatherInfo(data); |
Conclusion
The Eerie Weather App is live. Search for your city, crank up the thunderstorm intensity, and let the lightning illuminate your screen. Whether you’re looking for Three.js inspiration or just want the most dramatic way to check the weather, this app has you covered.