import * as THREE from 'three'; import type { EnvironmentChapter } from '@/journey/types'; import { SIGN_ACCENTS } from '@/journey/sign-routes'; export interface ChapterPaletteDefinition { sky: number; fog: number; terrainLow: number; terrainHigh: number; sun: number; haze: number; warmth: number; terrainEnergy: number; starVisibility: number; streakEnergy: number; } export interface MutableChapterPalette { sky: MutableColor; fog: MutableColor; terrainLow: MutableColor; terrainHigh: MutableColor; sun: MutableColor; haze: MutableColor; warmth: number; terrainEnergy: number; starVisibility: number; streakEnergy: number; } export interface MutableColor { clone(): MutableColor; copy(source: MutableColor): MutableColor; getHex(): number; lerp(target: MutableColor, progress: number): MutableColor; lerpColors(from: MutableColor, to: MutableColor, progress: number): MutableColor; setHex(value: number): MutableColor; } export const CHAPTER_PALETTES: Readonly> = { 'deep-night': { sky: 0x19092e, fog: 0x291236, terrainLow: 0x48cbe0, terrainHigh: 0xf16cbd, sun: 0xff649d, haze: 0xaf287d, warmth: 0.12, terrainEnergy: 0.78, starVisibility: 1, streakEnergy: 0.72, }, systems: { sky: 0x140b34, fog: 0x221c48, terrainLow: 0x59bdeb, terrainHigh: 0xe06fff, sun: 0xff569d, haze: 0x902dad, warmth: 0.22, terrainEnergy: 0.9, starVisibility: 0.94, streakEnergy: 0.86, }, infrastructure: { sky: 0x0e123b, fog: 0x252148, terrainLow: 0x3fe5dc, terrainHigh: 0xd179ff, sun: 0xff688e, haze: 0x773bb1, warmth: 0.3, terrainEnergy: 0.88, starVisibility: 0.86, streakEnergy: 0.82, }, builds: { sky: 0x260b37, fog: 0x391a43, terrainLow: 0x6eb8ff, terrainHigh: 0xff70ac, sun: 0xff8269, haze: 0xcb3a84, warmth: 0.46, terrainEnergy: 1, starVisibility: 0.72, streakEnergy: 1, }, 'pre-dawn': { sky: 0x261337, fog: 0x3a2648, terrainLow: 0x79c7ee, terrainHigh: 0xef81b7, sun: 0xffa567, haze: 0xbc536f, warmth: 0.76, terrainEnergy: 0.72, starVisibility: 0.38, streakEnergy: 0.52, }, destination: { sky: 0x311635, fog: 0x4d2a42, terrainLow: 0x77d3d7, terrainHigh: 0xe293c3, sun: 0xffc078, haze: 0xd96d74, warmth: 1, terrainEnergy: 0.54, starVisibility: 0.16, streakEnergy: 0.18, }, }; /** * END OF SHIFT: the destination's sky at daybreak. Not a journey chapter: the * environment blends the destination toward it as the outro's dawn rises. */ export const DAYBREAK_PALETTE: Readonly = { sky: 0x6b4a7a, fog: 0xb07a8c, terrainLow: 0xffb98a, terrainHigh: 0xffe1c2, sun: 0xffd49a, haze: 0xffb07a, warmth: 1.25, terrainEnergy: 0.4, starVisibility: 0, streakEnergy: 0, }; const daybreakColors = { sky: new THREE.Color(DAYBREAK_PALETTE.sky), fog: new THREE.Color(DAYBREAK_PALETTE.fog), terrainLow: new THREE.Color(DAYBREAK_PALETTE.terrainLow), terrainHigh: new THREE.Color(DAYBREAK_PALETTE.terrainHigh), sun: new THREE.Color(DAYBREAK_PALETTE.sun), haze: new THREE.Color(DAYBREAK_PALETTE.haze), }; /** Leans a written palette toward daybreak (0 untouched, 1 full daybreak). */ export function blendDaybreak( target: MutableChapterPalette, amount: number, ): MutableChapterPalette { const mix = clamp01(amount); if (mix === 0) return target; target.sky.lerp(daybreakColors.sky, mix); target.fog.lerp(daybreakColors.fog, mix); target.terrainLow.lerp(daybreakColors.terrainLow, mix); target.terrainHigh.lerp(daybreakColors.terrainHigh, mix); target.sun.lerp(daybreakColors.sun, mix); target.haze.lerp(daybreakColors.haze, mix); target.warmth = lerp(target.warmth, DAYBREAK_PALETTE.warmth, mix); target.terrainEnergy = lerp(target.terrainEnergy, DAYBREAK_PALETTE.terrainEnergy, mix); target.starVisibility = lerp(target.starVisibility, DAYBREAK_PALETTE.starVisibility, mix); target.streakEnergy = lerp(target.streakEnergy, DAYBREAK_PALETTE.streakEnergy, mix); return target; } /** * Star visibility multiplier while the engine is off: the stars brighten as * the engine cuts (up to 1.8x), then drain back as the dawn rises. */ export function engineOffStarScale(engineOff: number, dawn: number): number { return 1 + 0.8 * clamp01(engineOff) * (1 - clamp01(dawn)); } /** * The 03:12 window's storm: a colder navy the Infrastructure sky leans toward * (about 55%) while the maintenance window is open. A tint, not a chapter. */ export const STORM_TINT = 0x060c26; /** DRIVE-IN: the billboard frame wears the Selected builds chapter accent. */ export const BILLBOARD_ACCENT: string = SIGN_ACCENTS['selected-builds']; /** Star visibility while the storm is in. */ export const STORM_STAR_SCALE = 0.4; const paletteColors = Object.fromEntries( Object.entries(CHAPTER_PALETTES).map(([chapter, palette]) => [chapter, { sky: new THREE.Color(palette.sky), fog: new THREE.Color(palette.fog), terrainLow: new THREE.Color(palette.terrainLow), terrainHigh: new THREE.Color(palette.terrainHigh), sun: new THREE.Color(palette.sun), haze: new THREE.Color(palette.haze), }]), ) as Readonly>>; export function createMutableChapterPalette( chapter: EnvironmentChapter = 'deep-night', ): MutableChapterPalette { const source = CHAPTER_PALETTES[chapter]; const colors = paletteColors[chapter]; return { sky: colors.sky.clone(), fog: colors.fog.clone(), terrainLow: colors.terrainLow.clone(), terrainHigh: colors.terrainHigh.clone(), sun: colors.sun.clone(), haze: colors.haze.clone(), warmth: source.warmth, terrainEnergy: source.terrainEnergy, starVisibility: source.starVisibility, streakEnergy: source.streakEnergy, }; } export function writeChapterPalette( target: MutableChapterPalette, from: EnvironmentChapter, to: EnvironmentChapter, progress: number, ): MutableChapterPalette { const start = CHAPTER_PALETTES[from]; const end = CHAPTER_PALETTES[to]; const startColors = paletteColors[from]; const endColors = paletteColors[to]; const mix = clamp01(progress); target.sky.lerpColors(startColors.sky, endColors.sky, mix); target.fog.lerpColors(startColors.fog, endColors.fog, mix); target.terrainLow.lerpColors(startColors.terrainLow, endColors.terrainLow, mix); target.terrainHigh.lerpColors(startColors.terrainHigh, endColors.terrainHigh, mix); target.sun.lerpColors(startColors.sun, endColors.sun, mix); target.haze.lerpColors(startColors.haze, endColors.haze, mix); target.warmth = lerp(start.warmth, end.warmth, mix); target.terrainEnergy = lerp(start.terrainEnergy, end.terrainEnergy, mix); target.starVisibility = lerp(start.starVisibility, end.starVisibility, mix); target.streakEnergy = lerp(start.streakEnergy, end.streakEnergy, mix); return target; } function lerp(from: number, to: number, progress: number): number { return from + (to - from) * progress; } function clamp01(value: number): number { return Number.isFinite(value) ? THREE.MathUtils.clamp(value, 0, 1) : 0; }