feat: immerse the portfolio in a chromatic synthwave night

Carry rose, violet, coral, electric blue and cyan through the interface, road, terrain, signs and atmosphere. Add a Blender-authored paint edition and matching runtime fallback captures. Interpolate atmospheric color on a small sky lattice and enrich existing speed effects without another rendering pass.

Preserve approved copy, project media, metadata, navigation contracts and adaptive audio. Verified with typecheck, client/SSR builds, validated prerendering, artifact and transfer checks, responsive Chrome interaction, fallback checks and Intel-backed performance sampling.
This commit is contained in:
2026-09-05 13:17:53 +03:00
parent 43e510341e
commit 5d5935a1f8
24 changed files with 693 additions and 130 deletions

View File

@@ -0,0 +1,12 @@
# Chromatic grand tourer
Original paint edition of the approved expressway coachwork at `43e5103`, descended from the evolution model at `c9ac36e`. No external models or textures are used.
- `nightdrive-chromatic.blend`: isolated **Chromatic Nightdrive** scene with editable geometry, rose and violet layered paint, sapphire glass, champagne wheel machining, and look-development lights.
- `author-chromatic.py`: appends the approved expressway source and changes its materials in an isolated scene. Set `__file__` to this script when executing in Blender. It writes only the chromatic library.
- `export-chromatic.py`: run with the chromatic scene loaded. Evaluates modifiers and batches by material and wheel, retaining four independently rotatable wheel parents. Writes the GLB and SHA-256 provenance.
- `nightdrive-chromatic.asset.json`: source lineage and export statistics.
Geometry is unchanged: 31,288 triangles and 19 material batches. Runtime studio reflections supply cool edge light and a rose rim over the exported enamel. Runtime emissive fill remains subtle; the body is lit paint, not an illuminated surface. The earlier Blender sources and exports are preserved.
The chromatic Static-mode JPEGs are direct captures of this portfolio’s runtime world at desktop and phone sizes. They contain no project evidence or invented imagery.

View File

@@ -0,0 +1,43 @@
"""Append the approved model as an isolated, editable chromatic paint edition.
Run in Blender with __file__ set to this script. Open projects are never saved.
"""
import bpy
from pathlib import Path
root_path = Path(__file__).resolve().parents[3]
source = root_path / 'assets/blender/expressway/nightdrive-expressway.blend'
with bpy.data.libraries.load(str(source), link=False) as (available, loaded):
loaded.scenes = ['Midnight Expressway']
scene = loaded.scenes[0]
scene.name = 'Chromatic Nightdrive'
bpy.context.window.scene = scene
materials = {m.name.split('.')[0]: m for o in scene.objects if o.type in {'MESH','CURVE'} for m in o.data.materials}
# Linear RGB: candy-rose enamel, violet shoulders, champagne machining,
# sapphire glass and dark plum mechanical recesses. Geometry stays approved.
for name, color, metallic, roughness in [
('M_Body_Gunmetal', (0.46, 0.023, 0.145, 1), 0.68, 0.24),
('M_Body_Haunch', (0.36, 0.035, 0.38, 1), 0.72, 0.22),
('M_Lower_Graphite', (0.029, 0.01, 0.047, 1), 0.57, 0.36),
('M_Wheel', (0.60, 0.40, 0.25, 1), 0.90, 0.23),
('M_Glass_Smoke', (0.013, 0.024, 0.082, 1), 0.38, 0.14),
]:
material = materials[name]
bsdf = material.node_tree.nodes.get('Principled BSDF')
bsdf.inputs['Base Color'].default_value = color
material.diffuse_color = color
bsdf.inputs['Metallic'].default_value = metallic
bsdf.inputs['Roughness'].default_value = roughness
if 'Body' in name:
bsdf.inputs['Coat Weight'].default_value = 0.58
bsdf.inputs['Coat Roughness'].default_value = 0.16
bsdf.inputs['Emission Color'].default_value = (0.035, 0.002, 0.015, 1)
bsdf.inputs['Emission Strength'].default_value = 0.16
for obj in scene.objects:
if obj.type == 'LIGHT' and obj.name.startswith('LOOKDEV'):
if 'Key' in obj.name: obj.data.color = (0.42, 0.70, 1.0)
if 'Rim' in obj.name: obj.data.color = (1.0, 0.12, 0.48)
if 'Top' in obj.name: obj.data.color = (1.0, 0.73, 0.65)
scene.world = scene.world.copy() if scene.world else None
if scene.world: scene.world.color = (0.045, 0.018, 0.07)
output = root_path / 'assets/blender/chromatic/nightdrive-chromatic.blend'
bpy.data.libraries.write(str(output), {scene}, fake_user=True, compress=True)
print('AUTHORED', scene.name, len(scene.objects), 'objects', output)

View File

@@ -0,0 +1,51 @@
"""Export isolated authored source into material batches; leave lookdev editable."""
import bpy, json, hashlib
from mathutils import Matrix, Vector
from pathlib import Path
root_path=Path(__file__).resolve().parents[3]
scene=bpy.data.scenes['Chromatic Nightdrive']
bpy.context.window.scene=scene
export_scene=bpy.data.scenes.new('Chromatic Export')
root=bpy.data.objects.new('evolution-chase-car',None); export_scene.collection.objects.link(root)
names={'M_Body_Gunmetal':'car-body','M_Body_Haunch':'body-haunches','M_Glass_Smoke':'rear-glass','M_Rear_Light':'rear-light-bar','M_Cyan_Accent':'car-underglow-source','M_Lower_Graphite':'body-trim','M_Wheel':'wheel-machining','M_Tire':'tires'}
depsgraph=bpy.context.evaluated_depsgraph_get(); groups={}
for obj in scene.objects:
if obj.type not in ['MESH','CURVE'] or obj.name.startswith('LOOKDEV'): continue
evaluated=obj.evaluated_get(depsgraph)
mesh=bpy.data.meshes.new_from_object(evaluated,depsgraph=depsgraph); mesh.transform(obj.matrix_world)
clone=bpy.data.objects.new('export-'+obj.name,mesh); export_scene.collection.objects.link(clone)
key=obj.data.materials[0].name.split('.')[0]
wheel=None
for axle in ['front','rear']:
for side in ['left','right']:
if any(token in obj.name for token in [f'{axle}-wheel-{side}', f'{axle}-rim-{side}', f'{axle}-rim-ring-{side}', f'wheel-{axle}-{side}']):
wheel=f'wheel-{axle}-{side}'
groups.setdefault((key,wheel),[]).append(clone)
bpy.context.window.scene=export_scene
wheel_roots={}
for (key,wheel),objects in groups.items():
bpy.ops.object.select_all(action='DESELECT')
for obj in objects: obj.select_set(True)
bpy.context.view_layer.objects.active=objects[0]
if len(objects)>1: bpy.ops.object.join()
joined=bpy.context.object; joined.name='evolution-'+names[key]
if wheel:
if wheel not in wheel_roots:
axle='front' if 'front' in wheel else 'rear'; side=-1 if 'left' in wheel else 1
wheel_root=bpy.data.objects.new(wheel,None); export_scene.collection.objects.link(wheel_root)
wheel_root.parent=root; wheel_root.location=(side*1.01,-1.62 if axle=='front' else 1.62,.42 if axle=='front' else .46)
wheel_roots[wheel]=wheel_root
wheel_root=wheel_roots[wheel]
joined.data.transform(Matrix.Translation(-wheel_root.location))
joined.parent=wheel_root
else: joined.parent=root
bpy.ops.object.select_all(action='SELECT')
output=root_path/'src/assets/nightdrive-grand-tourer-chromatic.glb'
bpy.ops.export_scene.gltf(filepath=str(output),export_format='GLB',use_selection=True,use_active_scene=True,export_apply=True,export_animations=False,export_cameras=False,export_lights=False)
triangles=0
for obj in export_scene.objects:
if obj.type=='MESH': obj.data.calc_loop_triangles(); triangles+=len(obj.data.loop_triangles)
provenance={'asset':'Nightdrive chromatic grand tourer','source':'nightdrive-chromatic.blend','base':'../expressway/nightdrive-expressway.blend at 43e5103; original evolution at c9ac36e','authorship':'Original procedural modelling in Blender for this portfolio; no external assets or generated project evidence.','editable_scene':'Chromatic Nightdrive','material_groups':len(groups),'independent_wheels':4,'triangles':triangles,'export':'../../../src/assets/nightdrive-grand-tourer-chromatic.glb','sha256':hashlib.sha256(output.read_bytes()).hexdigest()}
(root_path/'assets/blender/chromatic/nightdrive-chromatic.asset.json').write_text(json.dumps(provenance,indent=2)+'\n')
bpy.context.window.scene=scene
print(json.dumps(provenance))

View File

@@ -0,0 +1,12 @@
{
"asset": "Nightdrive chromatic grand tourer",
"source": "nightdrive-chromatic.blend",
"base": "../expressway/nightdrive-expressway.blend at 43e5103; original evolution at c9ac36e",
"authorship": "Original procedural modelling in Blender for this portfolio; no external assets or generated project evidence.",
"editable_scene": "Chromatic Nightdrive",
"material_groups": 19,
"independent_wheels": 4,
"triangles": 31288,
"export": "../../../src/assets/nightdrive-grand-tourer-chromatic.glb",
"sha256": "89000428d04868cf71a8591fdf9b5096bcd8e732742a6760e6aa58b6d0a9e317"
}

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import './styles/layout.css';
import './styles/components.css'; import './styles/components.css';
import './styles/world.css'; import './styles/world.css';
import './styles/evolution.css'; import './styles/evolution.css';
import './styles/chromatic.css';
import './styles/print.css'; import './styles/print.css';
defineProps<{ inputs: Readonly<ProductionInputs> }>(); defineProps<{ inputs: Readonly<ProductionInputs> }>();

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

View File

@@ -4,8 +4,8 @@ import { useSceneController, type SceneHostState } from '@/composables/useSceneC
import type { JourneySnapshot } from '@/journey/types'; import type { JourneySnapshot } from '@/journey/types';
import type { MotionPolicy, QualityTier } from '@/journey/motion-policy'; import type { MotionPolicy, QualityTier } from '@/journey/motion-policy';
import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks'; import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks';
import posterUrl from '@/assets/hero-expressway.jpg'; import posterUrl from '@/assets/hero-chromatic.jpg';
import mobilePosterUrl from '@/assets/hero-expressway-mobile.jpg'; import mobilePosterUrl from '@/assets/hero-chromatic-mobile.jpg';
declare const __PORTFOLIO_TEST_BUILD__: boolean; declare const __PORTFOLIO_TEST_BUILD__: boolean;
const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__; const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__;

View File

@@ -22,6 +22,8 @@ export interface AtmosphereWorld {
speed: number, speed: number,
acceleration: number, acceleration: number,
palette: Readonly<MutableChapterPalette>, palette: Readonly<MutableChapterPalette>,
elapsedSeconds?: number,
overdrive?: number,
): void; ): void;
} }
@@ -38,7 +40,12 @@ export function createAtmosphere(
group.name = 'atmosphere-world'; group.name = 'atmosphere-world';
const night = CHAPTER_PALETTES['deep-night']; const night = CHAPTER_PALETTES['deep-night'];
const skyGeometry = resources.own('geometry', new THREE.PlaneGeometry(2, 2)); // The sky contains only broad gradients: interpolate their color over a
// small lattice instead of evaluating trigonometry for every screen pixel.
const skyGeometry = resources.own(
'geometry',
new THREE.PlaneGeometry(2, 2, 48, 32),
);
const skyMaterial = resources.own( const skyMaterial = resources.own(
'material', 'material',
new THREE.ShaderMaterial({ new THREE.ShaderMaterial({
@@ -48,37 +55,53 @@ export function createAtmosphere(
uHazeColor: { value: new THREE.Color(night.haze) }, uHazeColor: { value: new THREE.Color(night.haze) },
uWarmth: { value: night.warmth }, uWarmth: { value: night.warmth },
uAspect: { value: 1 }, uAspect: { value: 1 },
uTime: { value: 0 },
uOverdrive: { value: 0 },
}, },
vertexShader: ` vertexShader: `
varying vec2 vSkyUv;
void main() {
vSkyUv = uv;
gl_Position = vec4(position.xy, 1.0, 1.0);
}
`,
fragmentShader: `
uniform vec3 uSkyColor; uniform vec3 uSkyColor;
uniform vec3 uFogColor; uniform vec3 uFogColor;
uniform vec3 uHazeColor; uniform vec3 uHazeColor;
uniform float uWarmth; uniform float uWarmth;
uniform float uAspect; uniform float uAspect;
varying vec2 vSkyUv; uniform float uTime;
uniform float uOverdrive;
varying vec3 vSkyColor;
void main() { void main() {
float verticalSky = smoothstep(0.08, 0.66, vSkyUv.y); float verticalSky = smoothstep(0.08, 0.66, uv.y);
vec3 lowerField = mix(uSkyColor, uFogColor, 0.34); vec3 lowerField = mix(uSkyColor, uFogColor, 0.34);
vec3 color = mix(lowerField * 0.72, uSkyColor * 1.2, verticalSky); vec3 color = mix(lowerField * 0.8, uSkyColor * 1.35, verticalSky);
float horizonBand = exp(-pow(abs(vSkyUv.y - 0.47) * 6.2, 2.0)); float horizonBand = exp(-pow(abs(uv.y - 0.47) * 6.2, 2.0));
float normalizedX = abs(vSkyUv.x - 0.5) * 2.0 * min(uAspect, 2.35) / 2.35; float normalizedX = abs(uv.x - 0.5) * 2.0 * min(uAspect, 2.35) / 2.35;
float centerGlow = 1.0 - smoothstep(0.18, 1.16, normalizedX); float centerGlow = 1.0 - smoothstep(0.18, 1.16, normalizedX);
color = mix( color = mix(
color, color,
uHazeColor * (0.72 + uWarmth * 0.14), uHazeColor * (0.72 + uWarmth * 0.14),
horizonBand * mix(0.18, 0.4, centerGlow) horizonBand * mix(0.23, 0.48, centerGlow)
); );
float topFalloff = smoothstep(0.54, 1.0, vSkyUv.y); // Broad auroral veils share the existing sky pass. Slow, unequal
// periods let the sky breathe without scrolling or camera movement.
float drift = sin(uv.x * 5.2 + uTime * 0.025) * 0.035;
float arc = uv.y - (0.66 + 0.19 * sin(uv.x * 2.8 - 0.35) + drift);
float veil = exp(-arc * arc * 82.0);
float innerVeil = exp(-pow((arc + 0.055) * 17.0, 2.0));
float side = smoothstep(0.15, 0.7, uv.x);
vec3 violet = vec3(0.23, 0.018, 0.50);
vec3 rose = vec3(0.50, 0.018, 0.17);
vec3 aurora = mix(violet, rose, 0.5 + 0.5 * sin(uv.x * 4.2 + uTime * 0.018));
color += aurora * veil * (0.10 + side * 0.11) * (1.0 + uOverdrive * 0.36);
color += vec3(0.018, 0.075, 0.29) * innerVeil * side * 0.12;
float topFalloff = smoothstep(0.54, 1.0, uv.y);
float vignette = smoothstep(0.36, 1.18, normalizedX); float vignette = smoothstep(0.36, 1.18, normalizedX);
color *= 1.0 - topFalloff * 0.28 - vignette * 0.08; color *= 1.0 - topFalloff * 0.28 - vignette * 0.08;
gl_FragColor = vec4(color, 1.0); vSkyColor = color;
gl_Position = vec4(position.xy, 1.0, 1.0);
}
`,
fragmentShader: `
varying vec3 vSkyColor;
void main() {
gl_FragColor = vec4(vSkyColor, 1.0);
#include <tonemapping_fragment> #include <tonemapping_fragment>
#include <colorspace_fragment> #include <colorspace_fragment>
} }
@@ -165,7 +188,8 @@ export function createAtmosphere(
void main() { void main() {
float radius = distance(gl_PointCoord, vec2(0.5)); float radius = distance(gl_PointCoord, vec2(0.5));
float point = 1.0 - smoothstep(0.2, 0.5, radius); float point = 1.0 - smoothstep(0.2, 0.5, radius);
gl_FragColor = vec4(uColor, point * vBrightness * uVisibility); vec3 starColor = mix(uColor, vec3(1.0, 0.40, 0.58), smoothstep(0.65, 1.0, vBrightness));
gl_FragColor = vec4(starColor, point * vBrightness * uVisibility);
#include <tonemapping_fragment> #include <tonemapping_fragment>
#include <colorspace_fragment> #include <colorspace_fragment>
} }
@@ -200,6 +224,7 @@ export function createAtmosphere(
uniform float uDistance; uniform float uDistance;
uniform float uAcceleration; uniform float uAcceleration;
varying float vTail; varying float vTail;
varying vec3 vStreakColor;
void main() { void main() {
vec3 streak = position; vec3 streak = position;
float depthCycle = mod( float depthCycle = mod(
@@ -208,6 +233,7 @@ export function createAtmosphere(
) + ${WORLD_FAR_Z.toFixed(1)}; ) + ${WORLD_FAR_Z.toFixed(1)};
streak.z = depthCycle - endFactor * (1.4 + uAcceleration * 31.0); streak.z = depthCycle - endFactor * (1.4 + uAcceleration * 31.0);
vTail = endFactor; vTail = endFactor;
vStreakColor = mix(vec3(1.0, 0.065, 0.39), vec3(0.035, 0.66, 1.0), smoothstep(-80.0, 80.0, position.x));
gl_Position = projectionMatrix * modelViewMatrix * vec4(streak, 1.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(streak, 1.0);
} }
`, `,
@@ -215,8 +241,9 @@ export function createAtmosphere(
uniform vec3 uColor; uniform vec3 uColor;
uniform float uVisibility; uniform float uVisibility;
varying float vTail; varying float vTail;
varying vec3 vStreakColor;
void main() { void main() {
gl_FragColor = vec4(uColor * (1.0 + uVisibility * 0.45), uVisibility * mix(0.08, 0.62, vTail)); gl_FragColor = vec4(vStreakColor * (1.0 + uVisibility * 0.45), uVisibility * mix(0.08, 0.62, vTail));
#include <tonemapping_fragment> #include <tonemapping_fragment>
#include <colorspace_fragment> #include <colorspace_fragment>
} }
@@ -241,7 +268,14 @@ export function createAtmosphere(
: 1; : 1;
skyMaterial.uniforms.uAspect.value = safeAspect; skyMaterial.uniforms.uAspect.value = safeAspect;
}, },
update(distance, speed, acceleration, palette) { update(
distance,
speed,
acceleration,
palette,
elapsedSeconds = 0,
overdrive = 0,
) {
const safeAcceleration = clamp01(acceleration); const safeAcceleration = clamp01(acceleration);
const visibility = const visibility =
safeAcceleration * safeAcceleration *
@@ -257,6 +291,8 @@ export function createAtmosphere(
skyMaterial.uniforms.uFogColor.value.copy(palette.fog); skyMaterial.uniforms.uFogColor.value.copy(palette.fog);
skyMaterial.uniforms.uHazeColor.value.copy(palette.haze); skyMaterial.uniforms.uHazeColor.value.copy(palette.haze);
skyMaterial.uniforms.uWarmth.value = palette.warmth; skyMaterial.uniforms.uWarmth.value = palette.warmth;
skyMaterial.uniforms.uTime.value = finiteOrZero(elapsedSeconds);
skyMaterial.uniforms.uOverdrive.value = clamp01(overdrive);
for (let index = 0; index < silhouetteMaterials.length; index += 1) { for (let index = 0; index < silhouetteMaterials.length; index += 1) {
silhouetteMaterials[index]?.color silhouetteMaterials[index]?.color
.copy(palette.fog) .copy(palette.fog)

View File

@@ -1,6 +1,6 @@
import * as THREE from 'three'; import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import grandTourerUrl from '@/assets/nightdrive-grand-tourer-expressway.glb?url'; import grandTourerUrl from '@/assets/nightdrive-grand-tourer-chromatic.glb?url';
import type { SceneResourceBag } from '@/scene/resources'; import type { SceneResourceBag } from '@/scene/resources';
import type { SceneDetailMode } from '@/scene/types'; import type { SceneDetailMode } from '@/scene/types';
@@ -94,8 +94,8 @@ export function createCar(
const bodyMaterial = resources.own( const bodyMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x304e68, color: 0xa72c73,
emissive: 0x091725, emissive: 0x2c0822,
emissiveIntensity: 0.42, emissiveIntensity: 0.42,
roughness: 0.36, roughness: 0.36,
metalness: 0.68, metalness: 0.68,
@@ -104,8 +104,8 @@ export function createCar(
const shoulderMaterial = resources.own( const shoulderMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x365b77, color: 0x793fb2,
emissive: 0x0a1b2a, emissive: 0x211032,
emissiveIntensity: 0.36, emissiveIntensity: 0.36,
roughness: 0.3, roughness: 0.3,
metalness: 0.74, metalness: 0.74,
@@ -475,11 +475,11 @@ function createAuthoredCar(
material.isMeshStandardMaterial && material.isMeshStandardMaterial &&
(object.name === 'car-body' || object.name === 'body-haunches') (object.name === 'car-body' || object.name === 'body-haunches')
) { ) {
material.emissive?.setHex(0x0b2738); material.emissive?.setHex(0x491034);
material.emissiveIntensity = 0.26; material.emissiveIntensity = 0.16;
} }
if (material.isMeshStandardMaterial && object.name === 'rear-glass') { if (material.isMeshStandardMaterial && object.name === 'rear-glass') {
material.emissive?.setHex(0x041725); material.emissive?.setHex(0x0f143d);
material.emissiveIntensity = 0.31; material.emissiveIntensity = 0.31;
} }
if (object.name === 'rear-light-bar' && material.isMeshStandardMaterial) { if (object.name === 'rear-light-bar' && material.isMeshStandardMaterial) {

View File

@@ -6,13 +6,13 @@ import type { SceneResourceBag } from '@/scene/resources';
*/ */
export function createCoachworkEnvironment(resources: SceneResourceBag, renderer: unknown) { export function createCoachworkEnvironment(resources: SceneResourceBag, renderer: unknown) {
const studio = new THREE.Scene(); const studio = new THREE.Scene();
studio.background = new THREE.Color(0x172738); studio.background = new THREE.Color(0x251435);
const geometry = new THREE.PlaneGeometry(1, 1); const geometry = new THREE.PlaneGeometry(1, 1);
const cards = [ const cards = [
{ color: 0xb6dce8, power: 3, at: [-8, 7, 2], size: [4, 14] }, { color: 0x86d9ff, power: 3, at: [-8, 7, 2], size: [4, 14] },
{ color: 0x607c91, power: 1.5, at: [6, 5, -3], size: [5, 12] }, { color: 0xff68b7, power: 1.5, at: [6, 5, -3], size: [5, 12] },
{ color: 0xcbd8df, power: 2.2, at: [0, 12, 0], size: [14, 6] }, { color: 0xffdcce, power: 2.2, at: [0, 12, 0], size: [14, 6] },
{ color: 0xbc7278, power: 1.2, at: [0, 3, -12], size: [20, 4] }, { color: 0xff8b66, power: 1.2, at: [0, 3, -12], size: [20, 4] },
]; ];
const materials = cards.map((card) => { const materials = cards.map((card) => {
const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(card.color).multiplyScalar(card.power), side: THREE.DoubleSide }); const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(card.color).multiplyScalar(card.power), side: THREE.DoubleSide });

View File

@@ -317,6 +317,8 @@ export async function createProceduralSceneGeneration(
visualSpeed, visualSpeed,
response.streakEnergy, response.streakEnergy,
currentPalette, currentPalette,
atmosphericTime,
overdrive,
); );
background.copy(currentPalette.sky); background.copy(currentPalette.sky);
scene.fog.color.copy(currentPalette.fog); scene.fog.color.copy(currentPalette.fog);
@@ -325,9 +327,9 @@ export async function createProceduralSceneGeneration(
.lerp(currentPalette.terrainHigh, 0.26); .lerp(currentPalette.terrainHigh, 0.26);
ambient.groundColor.copy(currentPalette.fog); ambient.groundColor.copy(currentPalette.fog);
ambient.intensity = 1.05 + currentPalette.warmth * 0.18; ambient.intensity = 1.05 + currentPalette.warmth * 0.18;
carKeyLight.color.setHex(0xb9e3ef); carKeyLight.color.setHex(0xb2d5ff);
carKeyLight.intensity = 32 + response.bloomEnergy * 3.6 + overdrive * 8; carKeyLight.intensity = 32 + response.bloomEnergy * 3.6 + overdrive * 8;
carFillLight.color.setHex(0x809ec1); carFillLight.color.setHex(0xff62ad);
carFillLight.intensity = 12.5 + currentPalette.warmth * 2.8; carFillLight.intensity = 12.5 + currentPalette.warmth * 2.8;
rearLight.intensity = 4.2 + response.suspensionEnergy * 1.4; rearLight.intensity = 4.2 + response.suspensionEnergy * 1.4;
const wideComposition = camera.aspect > 1.18; const wideComposition = camera.aspect > 1.18;

View File

@@ -51,10 +51,10 @@ export function createExpressway(
void main() { void main() {
float farFade = 1.0 - smoothstep(380.0, 660.0, vDepth); float farFade = 1.0 - smoothstep(380.0, 660.0, vDepth);
float nearFade = smoothstep(0.0, 9.0, vDepth); float nearFade = smoothstep(0.0, 9.0, vDepth);
vec3 cyan = vec3(0.15, 0.69, 0.76); vec3 cyan = mix(vec3(1.0, 0.025, 0.34), vec3(0.02, 0.78, 0.93), step(0.0, vWorld.x));
vec3 coral = vec3(1.0, 0.18, 0.095); vec3 coral = vec3(1.0, 0.18, 0.095);
float facing = max(0.0, dot(normalize(vNormal), normalize(vec3(-0.5, 0.9, 0.6)))); float facing = max(0.0, dot(normalize(vNormal), normalize(vec3(-0.5, 0.9, 0.6))));
vec3 color = ${luminous ? `mix(${coral ? 'coral' : 'cyan'}, coral, uWarmth * 0.12) * (1.3 + uEnergy * 0.5)` : 'vec3(0.015, 0.027, 0.044) + vec3(0.055, 0.10, 0.135) * pow(facing, 2.0)'}; vec3 color = ${luminous ? `mix(${coral ? 'coral' : 'cyan'}, coral, uWarmth * 0.12) * (1.3 + uEnergy * 0.5)` : 'vec3(0.028, 0.012, 0.046) + vec3(0.09, 0.055, 0.14) * pow(facing, 2.0)'};
${!luminous ? 'color += cyan * pow(max(0.0, vNormal.x), 2.0) * 0.028;' : ''} ${!luminous ? 'color += cyan * pow(max(0.0, vNormal.x), 2.0) * 0.028;' : ''}
if(farFade * nearFade < 0.02) discard; if(farFade * nearFade < 0.02) discard;
gl_FragColor = vec4(color, farFade * nearFade); gl_FragColor = vec4(color, farFade * nearFade);

View File

@@ -29,8 +29,10 @@ export function createHorizonAir(resources: SceneResourceBag) {
float ribbon=exp(-pow((vUv.y-0.45-wave)*5.6,2.0)); float ribbon=exp(-pow((vUv.y-0.45-wave)*5.6,2.0));
float thin=exp(-pow((vUv.y-0.51-wave)*37.0,2.0)); float thin=exp(-pow((vUv.y-0.51-wave)*37.0,2.0));
float horizontal=pow(max(0.0,sin(vUv.x*3.14159)),2.0); float horizontal=pow(max(0.0,sin(vUv.x*3.14159)),2.0);
vec3 color=mix(uColor,vec3(0.21,0.49,0.59),vUv.y*0.25); float colorWave=0.5+0.5*sin(vUv.x*7.0+uTime*0.035);
gl_FragColor=vec4(color,(ribbon*0.12+thin*0.026)*horizontal*(0.6+uWarmth*0.4)); vec3 color=mix(uColor,vec3(0.27,0.045,0.75),colorWave*0.55);
color=mix(color,vec3(0.05,0.52,0.75),smoothstep(0.54,0.82,vUv.y)*0.3);
gl_FragColor=vec4(color,(ribbon*0.16+thin*0.032)*horizontal*(0.7+uWarmth*0.3));
#include <tonemapping_fragment> #include <tonemapping_fragment>
#include <colorspace_fragment> #include <colorspace_fragment>
} }

View File

@@ -38,72 +38,72 @@ export interface MutableColor {
export const CHAPTER_PALETTES: Readonly<Record<EnvironmentChapter, ChapterPaletteDefinition>> = { export const CHAPTER_PALETTES: Readonly<Record<EnvironmentChapter, ChapterPaletteDefinition>> = {
'deep-night': { 'deep-night': {
sky: 0x040713, sky: 0x19092e,
fog: 0x07101c, fog: 0x291236,
terrainLow: 0x388596, terrainLow: 0x48cbe0,
terrainHigh: 0xa85e87, terrainHigh: 0xf16cbd,
sun: 0xe97985, sun: 0xff649d,
haze: 0x69425b, haze: 0xaf287d,
warmth: 0.12, warmth: 0.12,
terrainEnergy: 0.78, terrainEnergy: 0.78,
starVisibility: 1, starVisibility: 1,
streakEnergy: 0.72, streakEnergy: 0.72,
}, },
systems: { systems: {
sky: 0x050918, sky: 0x140b34,
fog: 0x081622, fog: 0x221c48,
terrainLow: 0x168aa0, terrainLow: 0x59bdeb,
terrainHigh: 0xa83f79, terrainHigh: 0xe06fff,
sun: 0xd05282, sun: 0xff569d,
haze: 0x6d2968, haze: 0x902dad,
warmth: 0.22, warmth: 0.22,
terrainEnergy: 0.9, terrainEnergy: 0.9,
starVisibility: 0.94, starVisibility: 0.94,
streakEnergy: 0.86, streakEnergy: 0.86,
}, },
infrastructure: { infrastructure: {
sky: 0x060a1b, sky: 0x0e123b,
fog: 0x091a2a, fog: 0x252148,
terrainLow: 0x197f98, terrainLow: 0x3fe5dc,
terrainHigh: 0xa13d7a, terrainHigh: 0xd179ff,
sun: 0xd45b82, sun: 0xff688e,
haze: 0x6f2d70, haze: 0x773bb1,
warmth: 0.3, warmth: 0.3,
terrainEnergy: 0.88, terrainEnergy: 0.88,
starVisibility: 0.86, starVisibility: 0.86,
streakEnergy: 0.82, streakEnergy: 0.82,
}, },
builds: { builds: {
sky: 0x0b0d22, sky: 0x260b37,
fog: 0x111d2d, fog: 0x391a43,
terrainLow: 0x1a8ba0, terrainLow: 0x6eb8ff,
terrainHigh: 0xb04479, terrainHigh: 0xff70ac,
sun: 0xe16d7a, sun: 0xff8269,
haze: 0x7f356c, haze: 0xcb3a84,
warmth: 0.46, warmth: 0.46,
terrainEnergy: 1, terrainEnergy: 1,
starVisibility: 0.72, starVisibility: 0.72,
streakEnergy: 1, streakEnergy: 1,
}, },
'pre-dawn': { 'pre-dawn': {
sky: 0x15142a, sky: 0x261337,
fog: 0x1c2433, fog: 0x3a2648,
terrainLow: 0x277f91, terrainLow: 0x79c7ee,
terrainHigh: 0x9c4b78, terrainHigh: 0xef81b7,
sun: 0xf09667, sun: 0xffa567,
haze: 0xa45468, haze: 0xbc536f,
warmth: 0.76, warmth: 0.76,
terrainEnergy: 0.72, terrainEnergy: 0.72,
starVisibility: 0.38, starVisibility: 0.38,
streakEnergy: 0.52, streakEnergy: 0.52,
}, },
destination: { destination: {
sky: 0x241b31, sky: 0x311635,
fog: 0x2b2b38, fog: 0x4d2a42,
terrainLow: 0x386f79, terrainLow: 0x77d3d7,
terrainHigh: 0x87506e, terrainHigh: 0xe293c3,
sun: 0xf4b06d, sun: 0xffc078,
haze: 0xbb6b65, haze: 0xd96d74,
warmth: 1, warmth: 1,
terrainEnergy: 0.54, terrainEnergy: 0.54,
starVisibility: 0.16, starVisibility: 0.16,

View File

@@ -47,10 +47,10 @@ export function createRoad(
uDistance: { value: 0 }, uDistance: { value: 0 },
uSpeed: { value: 0 }, uSpeed: { value: 0 },
uAcceleration: { value: 0 }, uAcceleration: { value: 0 },
uAsphalt: { value: new THREE.Color(0x03070f) }, uAsphalt: { value: new THREE.Color(0x090511) },
uAsphaltLift: { value: new THREE.Color(0x162535) }, uAsphaltLift: { value: new THREE.Color(0x231936) },
uEdgeColor: { value: new THREE.Color(0x64d9e7) }, uEdgeColor: { value: new THREE.Color(0x47e4f0) },
uLaneColor: { value: new THREE.Color(0xf1a35e) }, uLaneColor: { value: new THREE.Color(0xffb568) },
uFogColor: { value: new THREE.Color(initialPalette.fog) }, uFogColor: { value: new THREE.Color(initialPalette.fog) },
uHazeColor: { value: new THREE.Color(initialPalette.haze) }, uHazeColor: { value: new THREE.Color(initialPalette.haze) },
}; };
@@ -110,6 +110,7 @@ export function createRoad(
void main() { void main() {
float roadX = abs(vRoadX); float roadX = abs(vRoadX);
vec3 edgeColor = mix(vec3(1.0, 0.035, 0.34), uEdgeColor, smoothstep(-1.0, 1.0, vRoadX));
float travelZ = -vWorldZ + mod(uDistance, ${FLOW_PERIOD.toFixed(1)}); float travelZ = -vWorldZ + mod(uDistance, ${FLOW_PERIOD.toFixed(1)});
float asphaltMask = 1.0 - smoothstep(7.78, 8.2, roadX); float asphaltMask = 1.0 - smoothstep(7.78, 8.2, roadX);
float shoulderMask = smoothstep(7.72, 8.08, roadX); float shoulderMask = smoothstep(7.72, 8.08, roadX);
@@ -137,8 +138,8 @@ export function createRoad(
vec3 shoulder = mix(uAsphalt * 0.54, uAsphaltLift * 0.7, broadVariation); vec3 shoulder = mix(uAsphalt * 0.54, uAsphaltLift * 0.7, broadVariation);
shoulder *= shoulderBreakup; shoulder *= shoulderBreakup;
vec3 color = mix(shoulder, asphalt, asphaltMask); vec3 color = mix(shoulder, asphalt, asphaltMask);
color += uEdgeColor * outerEdge * (0.78 + uAcceleration * 0.34); color += edgeColor * outerEdge * (0.78 + uAcceleration * 0.34);
color += uEdgeColor * shoulderMask * grain * 0.026; color += edgeColor * shoulderMask * grain * 0.026;
color += uHazeColor * shoulderSeam * (0.035 + aggregate * 0.025); color += uHazeColor * shoulderSeam * (0.035 + aggregate * 0.025);
color += uLaneColor * centerLane * (0.86 + uSpeed * 0.18); color += uLaneColor * centerLane * (0.86 + uSpeed * 0.18);
@@ -148,12 +149,12 @@ export function createRoad(
float streak = valueNoise(vec2(vRoadX * 16.0, travelZ * 0.19)); float streak = valueNoise(vec2(vRoadX * 16.0, travelZ * 0.19));
float film = mix(0.36, 1.0, streak) * (0.74 + 0.26 * fine); float film = mix(0.36, 1.0, streak) * (0.74 + 0.26 * fine);
float grazing = smoothstep(8.0, 120.0, vViewDistance); float grazing = smoothstep(8.0, 120.0, vViewDistance);
color += vec3(0.027, 0.047, 0.063) * film * asphaltMask * (0.35 + grazing * 0.65); color += vec3(0.037, 0.022, 0.069) * film * asphaltMask * (0.35 + grazing * 0.65);
float channel = exp(-pow((roadX - 7.98) * 2.4, 2.0)); float channel = exp(-pow((roadX - 7.98) * 2.4, 2.0));
color += uEdgeColor * channel * (0.06 + 0.10 * film); color += edgeColor * channel * (0.06 + 0.10 * film);
float shoulderRibs = step(0.77, fract(travelZ / 1.4)) float shoulderRibs = step(0.77, fract(travelZ / 1.4))
* smoothstep(8.28, 8.36, roadX) * (1.0 - smoothstep(8.85, 9.02, roadX)); * smoothstep(8.28, 8.36, roadX) * (1.0 - smoothstep(8.85, 9.02, roadX));
color += vec3(0.065, 0.10, 0.12) * shoulderRibs; color += vec3(0.11, 0.065, 0.17) * shoulderRibs;
float joint = 1.0 - smoothstep(0.02, 0.075, abs(mod(travelZ + 36.0, 72.0) - 36.0)); float joint = 1.0 - smoothstep(0.02, 0.075, abs(mod(travelZ + 36.0, 72.0) - 36.0));
color *= 1.0 - joint * 0.5; color *= 1.0 - joint * 0.5;
float studs = (1.0 - smoothstep(0.07, 0.14, abs(roadX - 7.53))) float studs = (1.0 - smoothstep(0.07, 0.14, abs(roadX - 7.53)))

View File

@@ -113,8 +113,8 @@ export function createSign(
const housingMaterial = resources.own( const housingMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x334956, color: 0x654169,
emissive: 0x23404e, emissive: 0x512547,
emissiveIntensity: 0.6, emissiveIntensity: 0.6,
roughness: 0.28, roughness: 0.28,
metalness: 0.7, metalness: 0.7,
@@ -126,7 +126,7 @@ export function createSign(
const backplateMaterial = resources.own( const backplateMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x172e3c, color: 0x301f47,
roughness: 0.72, roughness: 0.72,
metalness: 0.38, metalness: 0.38,
transparent: true, transparent: true,
@@ -137,7 +137,7 @@ export function createSign(
const facePanelMaterial = resources.own( const facePanelMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x061018, color: 0x11091d,
roughness: 0.9, roughness: 0.9,
metalness: 0.12, metalness: 0.12,
transparent: true, transparent: true,
@@ -148,8 +148,8 @@ export function createSign(
const supportMaterial = resources.own( const supportMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x21343e, color: 0x3c2752,
emissive: 0x163845, emissive: 0x38204e,
emissiveIntensity: 0.4, emissiveIntensity: 0.4,
roughness: 0.56, roughness: 0.56,
metalness: 0.68, metalness: 0.68,
@@ -161,8 +161,8 @@ export function createSign(
const edgeMaterial = resources.own( const edgeMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x39747a, color: 0xa3488d,
emissive: 0x55b9c0, emissive: 0xff79c0,
emissiveIntensity: 1.1, emissiveIntensity: 1.1,
roughness: 0.42, roughness: 0.42,
metalness: 0.5, metalness: 0.5,
@@ -174,8 +174,8 @@ export function createSign(
const routeMaterial = resources.own( const routeMaterial = resources.own(
'material', 'material',
new THREE.MeshStandardMaterial({ new THREE.MeshStandardMaterial({
color: 0x8e6446, color: 0xbc744d,
emissive: 0xb9723f, emissive: 0xffae6b,
emissiveIntensity: 0.8, emissiveIntensity: 0.8,
roughness: 0.48, roughness: 0.48,
metalness: 0.42, metalness: 0.42,
@@ -706,7 +706,7 @@ function drawLabel(
context.imageSmoothingQuality = 'high'; context.imageSmoothingQuality = 'high';
context.globalAlpha = 0.16; context.globalAlpha = 0.16;
context.fillStyle = '#8aa1a6'; context.fillStyle = '#cfabcd';
for (const [x, y, scratchWidth] of [ for (const [x, y, scratchWidth] of [
[312, 84, 142], [312, 84, 142],
[742, 424, 96], [742, 424, 96],
@@ -717,25 +717,25 @@ function drawLabel(
} }
context.globalAlpha = 1; context.globalAlpha = 1;
context.fillStyle = '#52bbc2'; context.fillStyle = '#ff95cd';
context.fillRect(82, 72, 10, 368); context.fillRect(82, 72, 10, 368);
context.fillStyle = '#91a6a9'; context.fillStyle = '#bbaae0';
context.font = SIGN_MARKER_FONT; context.font = SIGN_MARKER_FONT;
context.textAlign = 'left'; context.textAlign = 'left';
context.textBaseline = 'middle'; context.textBaseline = 'middle';
context.fillText(`ROUTE ${route.marker} / NEXT`, 142, 120); context.fillText(`ROUTE ${route.marker} / NEXT`, 142, 120);
context.globalAlpha = 0.6; context.globalAlpha = 0.6;
context.fillStyle = '#2f4650'; context.fillStyle = '#61416d';
context.fillRect(142, 174, 1_610, 2); context.fillRect(142, 174, 1_610, 2);
context.globalAlpha = 1; context.globalAlpha = 1;
context.fillStyle = '#e2f7f4'; context.fillStyle = '#fff2df';
const labelSize = fittedLabelSize(context, route.label, 1_520); const labelSize = fittedLabelSize(context, route.label, 1_520);
context.font = `650 ${labelSize}px "Geist Variable", sans-serif`; context.font = `650 ${labelSize}px "Geist Variable", sans-serif`;
context.fillText(route.label, 142, 326); context.fillText(route.label, 142, 326);
context.strokeStyle = '#cb9160'; context.strokeStyle = '#ffb879';
context.lineWidth = 10; context.lineWidth = 10;
context.lineCap = 'square'; context.lineCap = 'square';
context.lineJoin = 'miter'; context.lineJoin = 'miter';

View File

@@ -60,8 +60,8 @@ export function createSpeedWake(resources: SceneResourceBag) {
float crossFade=pow(max(0.0,1.0-abs(vUv.x*2.0-1.0)),1.3); float crossFade=pow(max(0.0,1.0-abs(vUv.x*2.0-1.0)),1.3);
float tip=pow(1.0-vUv.y,1.4)*smoothstep(0.0,0.055,vUv.y); float tip=pow(1.0-vUv.y,1.4)*smoothstep(0.0,0.055,vUv.y);
float filament=0.76+0.24*sin(vUv.y*35.0-uTime*15.0); float filament=0.76+0.24*sin(vUv.y*35.0-uTime*15.0);
vec3 color=mix(vec3(0.45,0.91,1.0),vec3(0.66,0.12,0.64),vUv.y); vec3 color=mix(vec3(0.08,0.86,1.0),vec3(1.0,0.02,0.44),vUv.y);
color+=vec3(0.7,0.85,0.9)*pow(crossFade,5.0)*0.7; color+=vec3(1.0,0.46,0.21)*pow(crossFade,5.0)*0.36;
gl_FragColor=vec4(color*1.8,crossFade*tip*filament*uPower*0.8); gl_FragColor=vec4(color*1.8,crossFade*tip*filament*uPower*0.8);
#include <tonemapping_fragment> #include <tonemapping_fragment>
#include <colorspace_fragment> #include <colorspace_fragment>
@@ -95,12 +95,13 @@ export function createSpeedWake(resources: SceneResourceBag) {
'material', 'material',
new THREE.ShaderMaterial({ new THREE.ShaderMaterial({
uniforms, uniforms,
vertexShader: `uniform float uPower;uniform float uTime;varying float vAlpha; vertexShader: `uniform float uPower;uniform float uTime;varying float vAlpha;varying vec3 vColor;
void main(){float phase=fract(uTime*0.66+position.y*0.5); void main(){float phase=fract(uTime*0.66+position.y*0.5);
vec3 p=vec3(position.x*(0.85+phase*0.85),0.04,position.z+phase*15.0); vec3 p=vec3(position.x*(0.85+phase*0.85),0.04,position.z+phase*15.0);
vColor=mix(vec3(1.0,0.06,0.4),vec3(0.04,0.85,1.0),position.y);
vAlpha=sin(phase*3.14159)*(1.0-phase)*uPower*uPower; vAlpha=sin(phase*3.14159)*(1.0-phase)*uPower*uPower;
gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.0);}`, gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.0);}`,
fragmentShader: `varying float vAlpha;void main(){gl_FragColor=vec4(0.3,0.85,1.0,vAlpha*0.48);}`, fragmentShader: `varying float vAlpha;varying vec3 vColor;void main(){gl_FragColor=vec4(vColor,vAlpha*0.48);}`,
transparent: true, transparent: true,
depthWrite: false, depthWrite: false,
depthTest: true, depthTest: true,

View File

@@ -73,9 +73,9 @@ export function createSun(resources: SceneResourceBag): SunWorld {
float lip = exp(-abs(phase - gap - 0.025) * 90.0) * step(solar.y, 0.65); float lip = exp(-abs(phase - gap - 0.025) * 90.0) * step(solar.y, 0.65);
float convection = sin(solar.x * 19.0 + sin(solar.y * 23.0 + uTime * 0.055)) float convection = sin(solar.x * 19.0 + sin(solar.y * 23.0 + uTime * 0.055))
* sin(solar.y * 31.0 - uTime * 0.032); * sin(solar.y * 31.0 - uTime * 0.032);
vec3 vermilion = vec3(0.91, 0.045, 0.075); vec3 vermilion = vec3(0.85, 0.018, 0.28);
vec3 coral = mix(uColor, vec3(1.0, 0.19, 0.10), 0.6); vec3 coral = mix(uColor, vec3(1.0, 0.16, 0.13), 0.48);
vec3 crown = vec3(1.0, 0.54, 0.25); vec3 crown = vec3(1.0, 0.63, 0.24);
vec3 core = mix(vermilion, coral, smoothstep(0.0, 0.58, solar.y)); vec3 core = mix(vermilion, coral, smoothstep(0.0, 0.58, solar.y));
core = mix(core, crown, smoothstep(0.47, 1.0, solar.y)); core = mix(core, crown, smoothstep(0.47, 1.0, solar.y));
float filament = pow(abs(sin(solar.x * 39.0 + solar.y * 25.0 + convection * 2.4)), 16.0); float filament = pow(abs(sin(solar.x * 39.0 + solar.y * 25.0 + convection * 2.4)), 16.0);
@@ -199,7 +199,7 @@ export function createSun(resources: SceneResourceBag): SunWorld {
: 0; : 0;
uniforms.uDriveEnergy.value = energy; uniforms.uDriveEnergy.value = energy;
limbMaterial.uniforms.uEnergy.value = energy; limbMaterial.uniforms.uEnergy.value = energy;
haloMaterial.opacity = 0.075 + palette.warmth * 0.03 + energy * 0.015; haloMaterial.opacity = 0.095 + palette.warmth * 0.03 + energy * 0.015;
bleedMaterial.opacity = 0.055 + palette.warmth * 0.03 + energy * 0.01; bleedMaterial.opacity = 0.055 + palette.warmth * 0.03 + energy * 0.01;
halo.scale.set(480 + energy * 18, 360 + energy * 10, 1); halo.scale.set(480 + energy * 18, 360 + energy * 10, 1);
disc.position.y = 79 + palette.warmth * 14; disc.position.y = 79 + palette.warmth * 14;

View File

@@ -90,12 +90,13 @@ export function createTerrain(
// produces a color seam. Compute it per vertex, not per pixel. // produces a color seam. Compute it per vertex, not per pixel.
vec3 gridSpectrum(float phase) { vec3 gridSpectrum(float phase) {
float p = fract(phase); float p = fract(phase);
vec3 cyan = vec3(0.025, 0.70, 0.90); vec3 cyan = vec3(0.015, 0.82, 0.94);
vec3 color = mix(cyan, vec3(0.075, 0.18, 0.80), smoothstep(0.04, 0.22, p)); vec3 color = mix(cyan, vec3(0.025, 0.12, 1.0), smoothstep(0.04, 0.22, p));
color = mix(color, vec3(0.39, 0.085, 0.80), smoothstep(0.22, 0.40, p)); color = mix(color, vec3(0.48, 0.025, 1.0), smoothstep(0.22, 0.40, p));
color = mix(color, vec3(0.88, 0.035, 0.28), smoothstep(0.40, 0.60, p)); color = mix(color, vec3(1.0, 0.02, 0.42), smoothstep(0.40, 0.60, p));
color = mix(color, vec3(0.95, 0.21, 0.08), smoothstep(0.60, 0.78, p)); color = mix(color, vec3(1.0, 0.22, 0.045), smoothstep(0.60, 0.78, p));
return mix(color, cyan, smoothstep(0.78, 1.0, p)); color = mix(color, vec3(1.0, 0.55, 0.10), smoothstep(0.78, 0.87, p));
return mix(color, cyan, smoothstep(0.87, 1.0, p));
} }
float hash21(vec2 point) { float hash21(vec2 point) {
@@ -174,7 +175,7 @@ export function createTerrain(
vec3 color = mix(vGridColor, uLowColor, 0.06); vec3 color = mix(vGridColor, uLowColor, 0.06);
// Speed warms passing accents, while the full palette stays present. // Speed warms passing accents, while the full palette stays present.
float hotAccent = uOverdrive * smoothstep(0.62, 1.0, vFlow) * 0.36; float hotAccent = uOverdrive * smoothstep(0.62, 1.0, vFlow) * 0.36;
color = mix(color, vec3(1.0, 0.28, 0.10), hotAccent); color = mix(color, vec3(1.0, 0.20, 0.25), hotAccent);
color = mix(color, uHighColor, ridgeMix * 0.055); color = mix(color, uHighColor, ridgeMix * 0.055);
color = mix(color, mix(uFogColor, uHazeColor, 0.2), 1.0 - vDepthFade); color = mix(color, mix(uFogColor, uHazeColor, 0.2), 1.0 - vDepthFade);
float pulse = 0.88 + 0.12 * smoothstep(0.16, 0.55, vFlow); float pulse = 0.88 + 0.12 * smoothstep(0.16, 0.55, vFlow);
@@ -184,7 +185,7 @@ export function createTerrain(
float surfaceStrength = mix(0.72, 0.12 + uOverdrive * 0.3, road); float surfaceStrength = mix(0.72, 0.12 + uOverdrive * 0.3, road);
float alpha = vDepthFade * pulse * routeCalm * mix(surfaceStrength, 0.91, ridgeMix); float alpha = vDepthFade * pulse * routeCalm * mix(surfaceStrength, 0.91, ridgeMix);
float charge = uOverdrive * (0.35 + vFlow * 0.8) * (0.5 + apron * 0.5); float charge = uOverdrive * (0.35 + vFlow * 0.8) * (0.5 + apron * 0.5);
gl_FragColor = vec4(color * (1.3 + ridgeMix * 0.5 + charge), alpha); gl_FragColor = vec4(color * (1.46 + ridgeMix * 0.54 + charge), alpha);
#include <tonemapping_fragment> #include <tonemapping_fragment>
#include <colorspace_fragment> #include <colorspace_fragment>
} }
@@ -223,10 +224,11 @@ export function createTerrain(
varying float vHeight; varying float vHeight;
varying float vDepthFade; varying float vDepthFade;
varying vec3 vTerrainPosition; varying vec3 vTerrainPosition;
varying vec3 vGridColor;
void main() { void main() {
vec3 normal = normalize(cross(dFdx(vTerrainPosition), dFdy(vTerrainPosition))); vec3 normal = normalize(cross(dFdx(vTerrainPosition), dFdy(vTerrainPosition)));
float light = abs(dot(normal, normalize(vec3(-0.5, 0.8, -0.3)))); float light = abs(dot(normal, normalize(vec3(-0.5, 0.8, -0.3))));
vec3 color = mix(vec3(0.002, 0.008, 0.016), uLowColor * 0.11, light); vec3 color = mix(vec3(0.012, 0.003, 0.024), vGridColor * 0.065, light);
color += uHighColor * pow(vHeight, 2.0) * light * 0.06; color += uHighColor * pow(vHeight, 2.0) * light * 0.06;
color = mix(uFogColor * 0.55, color, vDepthFade); color = mix(uFogColor * 0.55, color, vDepthFade);
gl_FragColor = vec4(color, 1.0); gl_FragColor = vec4(color, 1.0);

400
src/styles/chromatic.css Normal file
View File

@@ -0,0 +1,400 @@
@media screen {
/* Chromatic nightdrive: rose light, violet enamel and electric blue.
Color stays independent of layout, chapter measurements and drive state. */
:root {
--canvas: #130c23;
--surface: rgb(28 17 45 / 98.5%);
--surface-strong: #20142f;
--surface-soft: #2c1c40;
--line: rgb(202 147 238 / 24%);
--line-strong: rgb(231 129 215 / 54%);
--text: #fff4f1;
--text-muted: #cec0d7;
--accent: #8be9ed;
--accent-deep: #163943;
--focus: #ffe096;
--world-magenta: #ff70bf;
--world-amber: #ffbb86;
--rose: #ff83c2;
--coral: #ffa68d;
--violet: #c4a2ff;
--electric: #8bb8ff;
--night-ink-rgb: 19 10 33;
--chapter-accent: var(--rose);
--chapter-glow: 196 64 143;
--panel-shadow: 0 24px 80px rgb(13 2 28 / 34%);
}
.journey-world {
background: var(--canvas);
}
.site-navigation {
background: linear-gradient(
110deg,
rgb(39 19 52 / 97%),
rgb(22 22 48 / 97%)
);
border-color: rgb(198 122 209 / 34%);
box-shadow:
0 8px 32px rgb(13 2 28 / 24%),
inset 0 1px rgb(255 159 216 / 7%);
}
.site-navigation li {
--nav-accent: var(--coral);
}
.site-navigation li:nth-child(2),
.site-navigation li:nth-child(6) {
--nav-accent: var(--rose);
}
.site-navigation li:nth-child(3),
.site-navigation li:nth-child(7) {
--nav-accent: var(--violet);
}
.site-navigation li:nth-child(4) {
--nav-accent: var(--accent);
}
.site-navigation li:nth-child(5) {
--nav-accent: var(--electric);
}
.site-navigation a[aria-current="page"],
.site-navigation a:hover {
color: var(--nav-accent);
border-block-end-color: var(--nav-accent);
background: color-mix(in srgb, var(--nav-accent) 12%, transparent);
}
.site-navigation summary {
color: var(--rose);
}
.hero-journey h1 {
color: #ffb2d1;
background: linear-gradient(
158deg,
#fff1d5 8%,
#ffb3c5 42%,
#ff7eba 76%,
#c7a1ff 100%
);
background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-journey .section-eyebrow {
color: var(--coral);
}
.hero-journey__role {
color: #f7e4ff;
}
.hero-journey__value {
color: #d4c5e3;
}
.hero-journey__availability {
color: #c4b5d0;
}
.hero-journey__action {
color: var(--accent);
}
.hero-journey__action--primary,
.contact-link {
color: #291027;
border-color: #ff9dc1;
background: linear-gradient(115deg, #ffb783, #ff8fbb 72%, #e79ddd);
box-shadow:
0 6px 28px rgb(235 71 148 / 16%),
inset 0 1px rgb(255 230 210 / 50%);
}
.hero-journey__action--primary:hover,
.contact-link:hover {
color: #291027;
background: linear-gradient(115deg, #ffd298, #ffb3d0 72%, #f4c0e8);
}
#profile {
--chapter-accent: var(--rose);
--chapter-glow: 191 59 136;
}
#experience {
--chapter-accent: var(--violet);
--chapter-glow: 112 69 223;
}
#infrastructure {
--chapter-accent: var(--accent);
--chapter-glow: 39 94 183;
}
#selected-builds {
--chapter-accent: var(--coral);
--chapter-glow: 198 70 122;
}
#additional-work {
--chapter-accent: var(--rose);
--chapter-glow: 167 47 128;
}
#toolkit {
--chapter-accent: var(--violet);
--chapter-glow: 89 59 185;
}
#contact {
--chapter-accent: var(--coral);
--chapter-glow: 202 82 100;
}
.portfolio-section:not([data-layout="project-stage"]),
.portfolio-section[data-layout="full-diagram"],
.portfolio-section[data-layout="toolkit-field"] {
background:
radial-gradient(
ellipse at 100% 0%,
rgb(var(--chapter-glow) / 19%),
transparent 65%
),
var(--surface);
border-color: color-mix(in srgb, var(--chapter-accent) 28%, transparent);
box-shadow:
var(--panel-shadow),
inset 0 1px color-mix(in srgb, var(--chapter-accent) 38%, transparent);
}
.portfolio-section > h2 {
color: color-mix(in srgb, var(--chapter-accent) 30%, var(--text));
}
.portfolio-section .section-eyebrow,
.profile-facts dt,
.project-evidence dt {
color: var(--chapter-accent);
}
.portfolio-section[data-layout="project-stage"] > h2,
.portfolio-section[data-layout="project-stage"] > .section-eyebrow {
text-shadow: 0 2px 24px #160b29;
}
.portfolio-section li::marker {
color: var(--chapter-accent);
}
.experience-timeline::before {
background: linear-gradient(var(--violet), var(--rose) 58%, transparent);
}
.experience-timeline > li::before {
background: var(--rose);
border-color: var(--rose);
box-shadow: 0 0 16px rgb(255 93 180 / 30%);
}
.experience-record__meta {
color: #c8aee9;
}
.systems-operations {
background: linear-gradient(
115deg,
rgb(137 65 155 / 19%),
rgb(75 48 116 / 14%)
);
border-color: var(--rose);
}
.systems-operations h3 {
color: #f5c5e9;
}
.portfolio-section .infrastructure-flow-figure {
background: linear-gradient(145deg, #20213e, #262044 55%, #321e43);
box-shadow: inset 0 1px rgb(176 158 255 / 15%);
}
.infrastructure-node {
--node-accent: var(--electric);
}
.infrastructure-node--ingress {
--node-accent: var(--accent);
}
.infrastructure-node--access {
--node-accent: var(--violet);
}
.infrastructure-node--services {
--node-accent: var(--rose);
}
.infrastructure-node--operations {
--node-accent: var(--coral);
}
.infrastructure-node,
.infrastructure-node--operations {
background: color-mix(in srgb, var(--node-accent) 13%, #201a35);
border-color: color-mix(in srgb, var(--node-accent) 50%, transparent);
box-shadow:
inset 0 2px var(--node-accent),
0 12px 28px rgb(11 4 30 / 22%);
}
.infrastructure-node strong {
color: color-mix(in srgb, var(--node-accent) 38%, white);
}
.infrastructure-node__sequence,
.infrastructure-boundary,
.infrastructure-node__guard {
color: var(--node-accent);
}
.infrastructure-boundary::before {
border-color: var(--node-accent);
}
.infrastructure-node__connector {
background: var(--node-accent);
}
.infrastructure-node__connector::after {
border-color: var(--node-accent);
}
.operations-proof {
background: linear-gradient(125deg, #302340, #20283e);
border-color: rgb(202 151 239 / 28%);
}
.operations-proof > h4 {
color: #f6c7dc;
}
.project-case-study:nth-child(3n + 1) {
--chapter-accent: var(--coral);
--chapter-glow: 196 76 125;
}
.project-case-study:nth-child(3n + 2) {
--chapter-accent: var(--electric);
--chapter-glow: 64 77 188;
}
.project-case-study:nth-child(3n) {
--chapter-accent: var(--violet);
--chapter-glow: 121 59 190;
}
.project-case-study,
.project-case-study[data-project-priority="supporting"],
.project-case-study[data-project-id][data-media-layout] {
background:
radial-gradient(
ellipse at 90% 0%,
rgb(var(--chapter-glow) / 22%),
transparent 62%
),
var(--surface);
border-color: color-mix(in srgb, var(--chapter-accent) 30%, transparent);
box-shadow:
var(--panel-shadow),
inset 0 1px color-mix(in srgb, var(--chapter-accent) 46%, transparent);
}
.project-case-study__header {
border-inline-start-color: var(--chapter-accent);
}
.project-case-study__header h3 {
color: color-mix(in srgb, var(--chapter-accent) 30%, var(--text));
}
.project-media-gallery[data-media-layout="portrait-strip"] {
background: radial-gradient(
ellipse at 30% 0%,
#623457,
#392647 42%,
#252444 100%
);
border-color: rgb(240 137 195 / 28%);
}
.project-evidence > div {
border-color: color-mix(in srgb, var(--chapter-accent) 25%, transparent);
}
.project-link {
color: var(--chapter-accent);
background: color-mix(in srgb, var(--chapter-accent) 8%, transparent);
border-color: color-mix(in srgb, var(--chapter-accent) 45%, transparent);
}
.project-link:hover {
color: var(--text);
background: color-mix(in srgb, var(--chapter-accent) 19%, transparent);
}
.project-card:nth-child(even) {
--chapter-accent: var(--electric);
}
.project-card h3 {
color: color-mix(in srgb, var(--chapter-accent) 34%, var(--text));
}
.project-card {
border-color: color-mix(in srgb, var(--chapter-accent) 38%, transparent);
}
.toolkit-group {
--tool-color: var(--accent);
}
.toolkit-group:nth-child(2) {
--tool-color: var(--rose);
}
.toolkit-group:nth-child(3) {
--tool-color: var(--coral);
}
.toolkit-group:nth-child(4) {
--tool-color: var(--violet);
}
.toolkit-group h3 {
color: var(--tool-color);
}
.toolkit-item .brand-icon {
color: var(--tool-color);
}
.toolkit-item:hover {
background: color-mix(in srgb, var(--tool-color) 9%, transparent);
}
.contact-destination__copy {
color: #f7deed;
}
.contact-social-link {
color: #e4baf3;
}
.contact-social-link:hover {
color: var(--coral);
}
.drive-controls__dock,
.drive-controls__settings-trigger,
.journey-hud,
.drive-popover,
.drive-controls__audio-status {
background: linear-gradient(125deg, #2d193c, #1d1c35);
border-color: rgb(199 121 222 / 42%);
box-shadow:
0 8px 30px rgb(11 2 27 / 24%),
inset 0 1px rgb(255 162 226 / 8%);
}
.drive-start {
color: #201031;
border-color: #e49cd8;
background: linear-gradient(110deg, #f994c7, #cba5ff 65%, #9dccff);
box-shadow: inset 0 1px rgb(255 232 250 / 40%);
}
.drive-control-button {
background: #2a203e;
color: #d9c9e6;
}
.drive-control-button:hover {
background: #3d2853;
color: #ffa2d4;
}
.drive-control-button::after {
border-color: #6e437d;
background: #271633;
}
.drive-popover strong,
.drive-popover output {
color: var(--rose);
}
.drive-popover select {
background: #21182f;
border-color: #714d82;
}
.drive-popover input[type="range"] {
accent-color: var(--rose);
}
.journey-hud__route strong {
color: var(--coral);
}
.journey-hud__section {
color: var(--violet);
}
.journey-hud__arrival {
color: var(--coral);
}
}
@media (forced-colors: active) {
.hero-journey h1 {
background: none;
-webkit-text-fill-color: CanvasText;
}
}
@media print {
.hero-journey h1 {
background: none;
-webkit-text-fill-color: #111;
}
}

View File

@@ -781,9 +781,9 @@ body {
pointer-events: none; pointer-events: none;
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
rgb(8 13 24 / 98%), rgb(var(--night-ink-rgb, 8 13 24) / 98%),
rgb(8 13 24 / 88%) 31%, rgb(var(--night-ink-rgb, 8 13 24) / 88%) 31%,
rgb(8 13 24 / 25%) 51%, rgb(var(--night-ink-rgb, 8 13 24) / 25%) 51%,
transparent 70% transparent 70%
); );
} }
@@ -816,9 +816,9 @@ body {
inset-block-start: -70px; inset-block-start: -70px;
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
rgb(8 13 24 / 98%) 0%, rgb(var(--night-ink-rgb, 8 13 24) / 98%) 0%,
rgb(8 13 24 / 92%) 43%, rgb(var(--night-ink-rgb, 8 13 24) / 92%) 43%,
rgb(8 13 24 / 35%) 64%, rgb(var(--night-ink-rgb, 8 13 24) / 35%) 64%,
transparent 85% transparent 85%
); );
} }
@@ -852,9 +852,9 @@ body {
.hero-journey::before { .hero-journey::before {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
rgb(8 13 24 / 96%) 0%, rgb(var(--night-ink-rgb, 8 13 24) / 96%) 0%,
rgb(8 13 24 / 90%) 55%, rgb(var(--night-ink-rgb, 8 13 24) / 90%) 55%,
rgb(8 13 24 / 28%) 73%, rgb(var(--night-ink-rgb, 8 13 24) / 28%) 73%,
transparent 90% transparent 90%
); );
} }
@@ -863,9 +863,9 @@ body {
.hero-journey::before { .hero-journey::before {
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
rgb(8 13 24 / 98%) 0%, rgb(var(--night-ink-rgb, 8 13 24) / 98%) 0%,
rgb(8 13 24 / 92%) 53%, rgb(var(--night-ink-rgb, 8 13 24) / 92%) 53%,
rgb(8 13 24 / 48%) 66%, rgb(var(--night-ink-rgb, 8 13 24) / 48%) 66%,
transparent 85% transparent 85%
); );
} }