scene: shape each sky state's drawn sky into a luminance band its text ink clears #54

Merged
void merged 4 commits from nibu/sky-contrast into dev 2026-11-10 00:28:59 +00:00 AGit
Member

What

Per the operator's ruling (no plates; fix contrast in the sky itself): each fixed sky state's drawn sky is shaped into a luminance band (WCAG relative luminance, the luminance of src/sky/skyTokens.js) in which that state's theme text ink clears 4.5:1 on bare sky. Nothing was seen rendered: tests and numbers only. GL/GLSL was not run on a GPU (the Terrain GL fake pins the uploaded uniforms; the GLSL twin of the shaping is pinned by source-string tests, not by numeric evaluation).

Bands

State Text ink Needs Shaping target Drawn backdrop (skyBackdrop, darkest to lightest)
Day #342d25 bg >= 0.2983 floor 0.317 (linear luma) 0.461063 to 0.901108
Dusk #342d25 bg >= 0.2983 floor 0.317 0.30619 to 0.968744
Night #aeccff bg <= 0.0931 ceiling 0.085 0.05987 to 0.081705
  • Floors sit above the text bound for gamma-2.2 encoding vs the sRGB curve, 8-bit rounding and the sky pass's dither at its worst; the backdrops bound those readings too: displayedLuminance(c, 'low' | 'high') moves every encoded channel one 8-bit step down or up (rounding plus dither cannot move it further), a smooth bound that can be searched.
  • Link ink is not in the band (revised ruling). The baked night sky (0.058 to 0.077) is kept.
  • One shaping function, shapeToBand (skyBand.ts), with a GLSL twin (SHAPE_SKY_GLSL, terrainShaders.ts): below the floor it mixes toward a state tint by exactly the amount that lands on the floor (hue kept); above the ceiling it scales down. Applied to the final terrain sky colour, in the sky pass and in the haze that fades into it, and the 2D fallback inherits it through skyColour.
  • Ridge: dusk is now lavender, rose and orange (ridge/skyColour.ts); day and night use the baked gradients unchanged. The ridge is bounded over its whole range (stops and blends); it clears every floor without forcing (day 0.540, dusk 0.336 to 0.409, night 0.0599 to 0.0775).
  • Terrain: bounded over what its frozen camera reaches, elevation 0 to 16 degrees (TERRAIN_REACH_DEG, derived from CAMERA: top edge 14.5 plus 1.5 margin), every azimuth. Within that reach unshaped terrain dusk bottoms out at 0.2585; the lift takes it over the bound.
  • Auto resolves from the clock (sunElevation from sun.js, latitude 45, longitude from the timezone's standard-time offset; day at 6 degrees or more, night at -6 or less, dusk between). subscribe notifies the resolved state; getResolvedSkyState is the plain getter; skyBackdrop('auto') is the resolved state's band. One timer, only while a listener exists and the choice is auto. Auto does not override a signed-in person's theme: skyThemeId('auto') is undefined.
  • ?sun= ?az= ?sky= steer the terrain in auto (shaped into the band of the clock-resolved state); an explicit state beats the URL.

Measured ratios on bare sky (worst case over the backdrop)

State (auto = resolved state) Theme Text Link ink Link
Day Noonday Sky 6.60 #2a4f27 4.55
Dusk Noonday Sky 4.60 #2a4f27 3.17
Night Midnight Sky 4.89 #609385 2.28

Text clears 4.5 everywhere. The dusk and night link shortfall is Void's wiring: the link colours are derived by taking them toward black; the Night link is Void's to lighten to about 0.53 luminance or more. The numbers are pinned in skyTheme.test.ts as known values, not as >= 4.5.

Constants and how they are held

BACKDROPS in skyState.ts (constants, so the main chunk imports no terrain sky or baked tables) are the union of the ridge (whole range) and the terrain (camera reach, 0 to 16 degrees) as ridgeDrawn / terrainDrawn give them, rounded outward to six places. terrainDrawn carries #48's finer search (compass step halving to 5e-7 degrees). The drift test adds #48's independent cross-check, alternating golden-section line searches from 60 starts on the same screen reading, and holds each constant within 1e-6 above the swept extreme (a constant 1e-6 inward or 2e-5 outward fails, for all six). The entry chunk size was not measured; no production build was run.

Not done / findings

  • The nearest ridge layer is dark ground colour; dark ink over it would fail. The brief covered the sky only.
  • Plates are untouched: skyPlate*.ts is still wired into the signed-in UI (CallView, RoomIntro, shell regions); removal is a follow-up.
  • GLSL numeric parity is not evaluated (no GLSL evaluator); only source pins.
  • No useActiveTheme-level render test (no React render harness in the repo); the decision is pinned at skyThemeId.
  • chooseSky's URL path now also serves the door in auto; ridgeSkyFor ignores the URL as before.

🤖 Generated with Claude Code

## What Per the operator's ruling (no plates; fix contrast in the sky itself): each fixed sky state's drawn sky is shaped into a luminance band (WCAG relative luminance, the `luminance` of `src/sky/skyTokens.js`) in which that state's theme text ink clears 4.5:1 on bare sky. **Nothing was seen rendered: tests and numbers only. GL/GLSL was not run on a GPU** (the Terrain GL fake pins the uploaded uniforms; the GLSL twin of the shaping is pinned by source-string tests, not by numeric evaluation). ## Bands | State | Text ink | Needs | Shaping target | Drawn backdrop (`skyBackdrop`, darkest to lightest) | |---|---|---|---|---| | Day | #342d25 | bg >= 0.2983 | floor 0.317 (linear luma) | 0.461063 to 0.901108 | | Dusk | #342d25 | bg >= 0.2983 | floor 0.317 | 0.30619 to 0.968744 | | Night | #aeccff | bg <= 0.0931 | ceiling 0.085 | 0.05987 to 0.081705 | - Floors sit above the text bound for gamma-2.2 encoding vs the sRGB curve, 8-bit rounding and the sky pass's dither at its worst; the backdrops bound those readings too: `displayedLuminance(c, 'low' | 'high')` moves every encoded channel one 8-bit step down or up (rounding plus dither cannot move it further), a smooth bound that can be searched. - Link ink is not in the band (revised ruling). The baked night sky (0.058 to 0.077) is kept. - One shaping function, `shapeToBand` (`skyBand.ts`), with a GLSL twin (`SHAPE_SKY_GLSL`, `terrainShaders.ts`): below the floor it mixes toward a state tint by exactly the amount that lands on the floor (hue kept); above the ceiling it scales down. Applied to the final terrain sky colour, in the sky pass and in the haze that fades into it, and the 2D fallback inherits it through `skyColour`. - Ridge: dusk is now lavender, rose and orange (`ridge/skyColour.ts`); day and night use the baked gradients unchanged. The ridge is bounded over its whole range (stops and blends); it clears every floor without forcing (day 0.540, dusk 0.336 to 0.409, night 0.0599 to 0.0775). - Terrain: bounded over what its frozen camera reaches, elevation 0 to 16 degrees (`TERRAIN_REACH_DEG`, derived from `CAMERA`: top edge 14.5 plus 1.5 margin), every azimuth. Within that reach unshaped terrain dusk bottoms out at 0.2585; the lift takes it over the bound. - Auto resolves from the clock (`sunElevation` from `sun.js`, latitude 45, longitude from the timezone's standard-time offset; day at 6 degrees or more, night at -6 or less, dusk between). `subscribe` notifies the resolved state; `getResolvedSkyState` is the plain getter; `skyBackdrop('auto')` is the resolved state's band. One timer, only while a listener exists and the choice is auto. **Auto does not override a signed-in person's theme**: `skyThemeId('auto')` is undefined. - `?sun=` `?az=` `?sky=` steer the terrain in auto (shaped into the band of the clock-resolved state); an explicit state beats the URL. ## Measured ratios on bare sky (worst case over the backdrop) | State (auto = resolved state) | Theme | Text | Link ink | Link | |---|---|---|---|---| | Day | Noonday Sky | 6.60 | #2a4f27 | 4.55 | | Dusk | Noonday Sky | 4.60 | #2a4f27 | 3.17 | | Night | Midnight Sky | 4.89 | #609385 | 2.28 | Text clears 4.5 everywhere. The dusk and night link shortfall is Void's wiring: the link colours are derived by taking them toward black; the Night link is Void's to lighten to about 0.53 luminance or more. The numbers are pinned in `skyTheme.test.ts` as known values, not as >= 4.5. ## Constants and how they are held `BACKDROPS` in `skyState.ts` (constants, so the main chunk imports no terrain sky or baked tables) are the union of the ridge (whole range) and the terrain (camera reach, 0 to 16 degrees) as `ridgeDrawn` / `terrainDrawn` give them, rounded outward to six places. `terrainDrawn` carries #48's finer search (compass step halving to 5e-7 degrees). The drift test adds #48's independent cross-check, alternating golden-section line searches from 60 starts on the same screen reading, and holds each constant within 1e-6 above the swept extreme (a constant 1e-6 inward or 2e-5 outward fails, for all six). The entry chunk size was not measured; no production build was run. ## Not done / findings - The nearest ridge layer is dark ground colour; dark ink over it would fail. The brief covered the sky only. - Plates are untouched: `skyPlate*.ts` is still wired into the signed-in UI (CallView, RoomIntro, shell regions); removal is a follow-up. - GLSL numeric parity is not evaluated (no GLSL evaluator); only source pins. - No `useActiveTheme`-level render test (no React render harness in the repo); the decision is pinned at `skyThemeId`. - `chooseSky`'s URL path now also serves the door in auto; `ridgeSkyFor` ignores the URL as before. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
scene: keep the terrain sky and baked tables out of the main chunk
Some checks failed
Build pull request / Build pull request (pull_request) Has been cancelled
0d26b71862
skyBackdrop reads constants (the union of the ridge and terrain skies, rounded
outward) instead of computing it, and the door imports skyForState lazily with
the scenes. A test fails when the constants drift from the computed union.
Main entry chunk vs dev: +0.4 KB gzip, down from +51.5 KB.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scene: terrain backdrop range found by refinement, not a grid
Some checks failed
Build pull request / Build pull request (pull_request) Has been cancelled
8a05668d84
terrainDrawn finds the sun glow peak and the darkest patch with a compass
search from the best coarse samples; constants regenerated (Day 0.237309-0.92805,
Dusk 0.016727-0.967912). The drift test sweeps its own grid (golden-angle spiral
plus a 0.05 degree patch around the sun).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scene: shape each sky state's drawn sky into a luminance band its text ink clears
Some checks failed
Build pull request / Build pull request (pull_request) Has been cancelled
5d3db9e1f2
Day and dusk: floor 0.30 (dark ink needs 0.2983). Night: ceiling 0.0931 for the light ink; the baked night sky is kept.
One shaping function (skyBand.shapeToBand) with a GLSL twin lifts the terrain sky toward a tint (hue kept) or scales it down.
The ridge dusk becomes lavender, rose and orange. 'auto' resolves from the clock (sun.js elevation) to day, dusk or night;
subscribers hear the resolved sky; skyBackdrop('auto') is the resolved state's band. Link ink is not in the band (Void's wiring).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scene: bound the terrain sky over what its frozen camera reaches (0 to 16 degrees), ridge stays whole-range
Some checks failed
Build pull request / Build pull request (pull_request) Has been cancelled
4177bdcccb
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
scene: auto never picks a theme; URL steers auto; quantised bounds; listener-window fix; DST
Some checks failed
Build pull request / Build pull request (pull_request) Failing after 26s
577d795189
- skyThemeId('auto') is undefined again (auto must not override a signed-in theme); the resolved state stays readable by getResolvedSkyState and subscribe.
- In auto, ?sun= ?az= ?sky= reach the terrain again, shaped into the band of the clock-resolved state; an explicit state beats the URL.
- The sky-change notice is no longer swallowed by a listener arriving or leaving between the clock crossing and the tick.
- BACKDROPS bound what the 8-bit screen shows with the dither at its worst; shaping floor 0.317, night ceiling 0.085.
- The clock uses standard time, so daylight saving does not move the sun.
- A Terrain GL fake test pins uBand and uLift for each state.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Merge origin/dev into nibu/sky-contrast
Some checks failed
Build pull request / Build pull request (pull_request) Failing after 12m22s
Docker check / docker-build (pull_request) Failing after 10s
d03ec36107
Conflicts: skyState.ts (kept #54's constants, re-derived), skyForState.test.ts (kept #54's tests, added #48's independent golden-section cross-check and the 1e-6 tight drift test on the screen reading).
terrainDrawn carries #48's finer search (5e-7 degrees) on the screen reading; the reading is now one 8-bit step each way (smooth), not a rounded value, so it can be searched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
void merged commit ea6dee80ae into dev 2026-11-10 00:28:59 +00:00
void referenced this pull request from a commit 2026-11-10 00:29:00 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Ratatoskr!54
No description provided.