Draft: ridge night band (preview for Meta, do not merge) #105

Closed
void wants to merge 1 commit from void/night-band-preview into dev
6 changed files with 54 additions and 14 deletions

View file

@ -48,7 +48,7 @@ describe('startBackgroundScene follows the sky state', () => {
createRidgeScene.mockReturnValue(fakeScene());
const scene = await startBackgroundScene(config, caps, { canvas });
expect(createRidgeScene).toHaveBeenCalledOnce();
expect(hex(createRidgeScene.mock.calls[0][0].sky.top)).toBe(baked.midnight.gradient[0]);
expect(hex(createRidgeScene.mock.calls[0][0].sky.top)).toBe(hex(ridgeSkyFor('night').top));
scene?.dispose();
});
@ -67,7 +67,7 @@ describe('startBackgroundScene follows the sky state', () => {
setSkyState('night');
createRidgeScene.mockReturnValue(fakeScene());
const scene = await startBackgroundScene(config, caps, { canvas });
expect(hex(createRidgeScene.mock.calls[0][0].sky.top)).toBe(baked.midnight.gradient[0]);
expect(hex(createRidgeScene.mock.calls[0][0].sky.top)).toBe(hex(ridgeSkyFor('night').top));
scene?.dispose();
});

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { BakeParams, bakeSlice, faceLight, layerPaint, sunDirection } from './ridgeBake';
import { SKY_BAND } from '../skyBand';
import { RIDGE_NIGHT_BAND, SKY_BAND } from '../skyBand';
import { ridgeSkyFor } from '../skyForState';
import { LACUNARITY, LAYERS, profile } from './ridgeWorld';
import { skyColours, SkyColours } from './skyColour';
@ -357,6 +357,10 @@ describe('the state band: the theme ink clears 4.5:1 on a ridge as on bare sky',
expect(range(SKY_BAND.night).hi).toBeLessThanOrEqual(SKY_BAND.night.ceiling);
});
it("ridge night: no ridge pixel is lighter than the ridge's own, lower ceiling", () => {
expect(range(RIDGE_NIGHT_BAND).hi).toBeLessThanOrEqual(RIDGE_NIGHT_BAND.ceiling);
});
it('is the identity with no band, so nothing else changes', () => {
expect(layerPaint(LAYERS[2], SKY).shift).toBeNull();
expect(range(undefined).lo).toBeLessThan(SKY_BAND.day.floor);

View file

@ -73,6 +73,13 @@ export const SKY_BAND: Record<FixedSkyState, SkyBand> = {
night: { floor: 0, ceiling: 0.077394, lift: hexToLinear('#5a6a9a') },
};
/**
* The ridge sky's night band: the same night, with a lower ceiling. The foldout headers wear a dimmer ink than the
* page text (L 0.386 against 0.594), and that ink needs a backdrop of at most 0.047 for 4.5:1. The terrain sky keeps
* SKY_BAND.night.
*/
export const RIDGE_NIGHT_BAND: SkyBand = { ...SKY_BAND.night, ceiling: 0.04 };
/**
* Shape a linear sky colour into the band. Below the floor it mixes toward `lift` by exactly the amount
* that lands on the floor (continuous at the floor, hue kept); above the ceiling it scales down to the

View file

@ -12,7 +12,7 @@ import {
terrainDrawn,
terrainSkyFor,
} from './skyForState';
import { displayedLuminance, luma, shapeToBand, SKY_BAND } from './skyBand';
import { displayedLuminance, luma, RIDGE_NIGHT_BAND, shapeToBand, SKY_BAND } from './skyBand';
import type { FixedSkyState } from './skyBand';
import { setSkyClock, skyBackdrop } from './skyState';
import { view } from './terrain/terrainWorld';
@ -78,9 +78,22 @@ describe('the bands the text ink needs', () => {
describe('ridgeSkyFor', () => {
it('carries the state band, which the layers are shaped into', () => {
for (const state of ['day', 'dusk', 'night'] as const) {
expect(ridgeSkyFor(state).band).toEqual(SKY_BAND[state]);
}
expect(ridgeSkyFor('day').band).toEqual(SKY_BAND.day);
expect(ridgeSkyFor('dusk').band).toEqual(SKY_BAND.dusk);
expect(ridgeSkyFor('night').band).toBe(RIDGE_NIGHT_BAND);
expect(terrainSkyFor('night').band).toBe(SKY_BAND.night);
});
it("night's stops are shaped into the ridge's own ceiling, the hue kept", () => {
const night = ridgeSkyFor('night');
const baseline = [baked.midnight.gradient[0], baked.midnight.gradient[6]].map((h) =>
[1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16))
);
[night.top, night.horizon].forEach((stop, i) => {
expect(luminance(hex(stop))).toBeLessThanOrEqual(RIDGE_NIGHT_BAND.ceiling + 0.0005);
// The brightest channel is the same one: scaling down in linear light does not turn the hue.
expect(stop.indexOf(Math.max(...stop))).toBe(baseline[i].indexOf(Math.max(...baseline[i])));
});
});
it('draws the gradient of the sky auto resolves to, never nothing', () => {
@ -109,8 +122,11 @@ describe('ridgeSkyFor', () => {
const night = ridgeSkyFor('night');
expect(hex(day.top)).toBe(baked.noonday.gradient[0]);
expect(hex(day.horizon)).toBe(baked.noonday.gradient[6]);
expect(hex(night.top)).toBe(baked.midnight.gradient[0]);
expect(hex(night.horizon)).toBe(baked.midnight.gradient[6]);
// Night's stops are the baked ones shaped into the ridge's lower ceiling (next test), so only darker or equal.
expect(luminance(hex(night.top))).toBeLessThanOrEqual(luminance(baked.midnight.gradient[0]));
expect(luminance(hex(night.horizon))).toBeLessThanOrEqual(
luminance(baked.midnight.gradient[6])
);
});
it.each(STATES)('%s: every stop and blend lies in the band its text ink needs', (state) => {

View file

@ -10,7 +10,13 @@ import type { SkyColours, Srgb } from './ridge/skyColour';
import { skyColours } from './ridge/skyColour';
import { resolveSkyState } from './skyState';
import type { SkyState } from './skyState';
import { displayedLuminance, hexToLinear, shapeToBand, SKY_BAND } from './skyBand';
import {
displayedLuminance,
hexToLinear,
RIDGE_NIGHT_BAND,
shapeToBand,
SKY_BAND,
} from './skyBand';
import type { SkyBand } from './skyBand';
import { luminance } from '../../../sky/skyTokens.js';
import { CAMERA } from './terrain/terrainWorld';
@ -38,7 +44,7 @@ const toByte = (v: number): number =>
const inBand = (c: Srgb, band: SkyBand): Srgb =>
shapeToBand(hexToLinear(rgbHex(c)), band).map(toByte) as unknown as Srgb;
/** The ridge scene's `sky` option: the gradient of the state's sky ('auto': of the sky it resolves to). Dusk's twilight stops are shaped into its band. */
/** The ridge scene's `sky` option: the gradient of the state's sky ('auto': of the sky it resolves to). Dusk's twilight stops and night's baked stops are shaped into their bands (night's is the ridge's own, lower one). */
export function ridgeSkyFor(state: SkyState): SkyColours {
const { ground } = skyColours();
switch (resolveSkyState(state)) {
@ -55,8 +61,15 @@ export function ridgeSkyFor(state: SkyState): SkyColours {
}
case 'day':
return fromGradient(baked.noonday.gradient, ground, SKY_BAND.day);
default:
return fromGradient(baked.midnight.gradient, ground, SKY_BAND.night);
default: {
const night = fromGradient(baked.midnight.gradient, ground, RIDGE_NIGHT_BAND);
return {
...night,
top: inBand(night.top, RIDGE_NIGHT_BAND),
mid: inBand(night.mid, RIDGE_NIGHT_BAND),
horizon: inBand(night.horizon, RIDGE_NIGHT_BAND),
};
}
}
}

View file

@ -132,7 +132,7 @@ export interface SkyBackdrop {
const BACKDROPS: Record<FixedSkyState, SkyBackdrop> = {
day: { darkest: 0.461063, lightest: 0.901108 },
dusk: { darkest: 0.460352, lightest: 0.968744 },
night: { darkest: 0.05987, lightest: 0.081642 },
night: { darkest: 0.039159, lightest: 0.081642 },
};
export const skyBackdrop = (state: SkyState): SkyBackdrop => BACKDROPS[resolveSkyState(state)];