Files
the-matrix/js/daynight.js
Alexander Whitestone 87486d2c7f feat: add day/night cycle driven by real UTC time
- New js/daynight.js module: computes sun elevation from UTC time,
  drives a DirectionalLight sun arc (east → zenith → west), adjusts
  ambient color/intensity, fog color, and star opacity each frame.
- Sunrise ~06:00 UTC, sunset ~18:00 UTC with smooth transitions.
- Stars (existing starfield) fade during day and glow green at night.
- Agent emissive glow and point-light intensity boost at night via
  nightFactor passed to updateAgents().
- WebGL context-loss recovery: daynight refs disposed in disposeWorld /
  disposeEffects and re-initialised on rebuild.

Fixes #4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 14:06:37 -04:00

128 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Day/night cycle based on real UTC time.
*
* Sunrise ~06:00 UTC, Sunset ~18:00 UTC.
* Drives: sun directional light, ambient light, fog color, star visibility.
* Returns a nightFactor (0=day, 1=night) for consumers (e.g. agent glow).
*/
import * as THREE from 'three';
let sunLight = null;
let ambientRef = null;
let sceneRef = null;
let starMatRef = null;
/** Call once during world init. */
export function initDayNight(scene, ambientLight) {
sceneRef = scene;
ambientRef = ambientLight;
sunLight = new THREE.DirectionalLight(0xffffff, 0);
sunLight.position.set(0, 80, 0);
scene.add(sunLight);
}
/** Called by effects.js after it creates the star PointsMaterial. */
export function setStarMaterial(mat) {
starMatRef = mat;
}
/** Returns seconds elapsed since UTC midnight. */
function utcSeconds() {
const n = new Date();
return n.getUTCHours() * 3600 + n.getUTCMinutes() * 60 + n.getUTCSeconds();
}
/**
* Sun elevation angle: 1 at UTC noon, -1 at UTC midnight.
* Zero-crossings at 06:00 (dawn) and 18:00 (dusk).
*/
function sunElevation() {
const frac = utcSeconds() / 86400; // 01
return Math.sin(2 * Math.PI * (frac - 0.25));
}
/**
* Smooth step from a to b.
* @param {number} edge0
* @param {number} edge1
* @param {number} x
*/
function smoothstep(edge0, edge1, x) {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
/**
* Update all day/night-driven scene properties.
* Called every animation frame.
* @returns {number} nightFactor — 0 (full day) to 1 (full night)
*/
export function updateDayNight() {
const elev = sunElevation();
// dayFactor: 1 at full day (elev > 0.15), 0 at full night (elev < -0.15)
const dayFactor = smoothstep(-0.15, 0.15, elev);
const nightFactor = 1 - dayFactor;
// ── Sun directional light ─────────────────────────────────────────────────
if (sunLight) {
// Arc: east (positive X) at dawn, overhead at noon, west at dusk
const frac = utcSeconds() / 86400;
const angle = 2 * Math.PI * (frac - 0.25);
const sx = -Math.cos(angle) * 60;
const sy = Math.sin(angle) * 80;
const sz = -30;
sunLight.position.set(sx, Math.max(sy, 0.1), sz);
// Color: warm orange near horizon, muted matrix-green at zenith
const horizonBlend = 1 - Math.min(1, Math.abs(elev) / 0.4);
const r = (0.3 + horizonBlend * 0.7) * dayFactor;
const g = (0.6 - horizonBlend * 0.2) * dayFactor;
const b = (0.3 - horizonBlend * 0.25) * dayFactor;
sunLight.color.setRGB(r, g, b);
sunLight.intensity = dayFactor * 1.2;
}
// ── Ambient light ─────────────────────────────────────────────────────────
if (ambientRef) {
// Night: very dark greenish. Day: slightly brighter, cyan tint.
ambientRef.intensity = 0.35 + dayFactor * 0.35;
const ar = dayFactor * 0.04;
const ag = 0.07 + dayFactor * 0.05;
const ab = dayFactor * 0.06;
ambientRef.color.setRGB(ar, ag, ab);
}
// ── Fog color ─────────────────────────────────────────────────────────────
if (sceneRef && sceneRef.fog) {
const fr = dayFactor * 0.008;
const fg = dayFactor * 0.016;
const fb = dayFactor * 0.010;
sceneRef.fog.color.setRGB(fr, fg, fb);
}
// ── Stars ─────────────────────────────────────────────────────────────────
if (starMatRef) {
starMatRef.opacity = 0.05 + nightFactor * 0.80;
// Night: bright green. Day: nearly invisible dark green.
const sg = 0.05 + nightFactor * 0.55;
starMatRef.color.setRGB(0, sg, 0);
starMatRef.needsUpdate = true;
}
return nightFactor;
}
/** Remove sun light from scene and clear module refs. */
export function disposeDayNight() {
if (sunLight && sceneRef) {
sceneRef.remove(sunLight);
sunLight.dispose();
}
sunLight = null;
ambientRef = null;
sceneRef = null;
starMatRef = null;
}