Files
Portfolio/src/scene/world/palette.ts

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;
}