🌩️ Building a Haunted Weather App: A Spooky Journey into 3D Web Development

Published: (December 3, 2025 at 10:17 PM EST)
3 min read
Source: Dev.to

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#f4320b on 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}&current=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

ProblemSolution
Lightning bolts cause “Computed radius is NaN” errorsValidate coordinates before creating geometry: `if (isNaN(startX)
Rapid weather changes trigger play()/pause() conflictsTrack 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 selectionUse 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.

🌩️ Launch the Eerie Weather App

Back to Blog

Related posts

Read more »