242 lines
7.2 KiB
TypeScript
242 lines
7.2 KiB
TypeScript
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<Record<EnvironmentChapter, ChapterPaletteDefinition>> = {
|
|
'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<ChapterPaletteDefinition> = {
|
|
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<Record<EnvironmentChapter, Pick<MutableChapterPalette,
|
|
'sky' | 'fog' | 'terrainLow' | 'terrainHigh' | 'sun' | 'haze'>>>;
|
|
|
|
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;
|
|
}
|