Lighting & shadow model
themes specs/themes/lighting.kmd
The light source model behind every shadow in Koder Design. Defines WHERE light comes from (key light position + ambient), HOW shadows are cast (umbra/penumbra, direction, softness), and how dark mode RE-LIGHTS rather than just dimming shadows. Elevation tokens (`elevation.kmd`) are the rendered output of this model; this spec is the source they derive from. Material parity: the implicit light model behind M3 elevation, made explicit and tokenized.
Quando esta spec se aplica
Triggers primários
- Define or tune the light source for any Koder surface
Todos os triggers
- Author a new shadow recipe or elevation level
- Decide shadow direction / softness for a preset
- Re-light a surface for dark mode (not just dim the shadow)
- Add a cast shadow to a 3D / model-viewer / XR surface
Corpo da especificação
Spec — Lighting & shadow model
Facet Visual do Koder Design. Material parity: the light model implicit in https://m3.material.io/styles/elevation — Koder makes it explicit so shadows across 2D, 3D and XR stay coherent.
Elevation says which shadow a surface gets. Lighting says why that
shadow looks the way it does — one virtual light rig, so a card on
the web, a model-viewer mesh, and an XR panel all cast consistent
shadows. Every --shadow-* token in elevation.kmd is a 2D
projection of this model.
R1 — The light rig
Koder uses a two-source rig (Material/RealityKit convention):
| Source | Role | Default |
|---|---|---|
| Key light | Directional; casts the visible shadow | From top, slightly front and left: azimuth −30°, elevation 60° |
| Ambient light | Omnidirectional fill; lifts shadow floor | Soft, ~40% intensity; no direction |
Consequence (canonical): shadows fall down and slightly right of
the element. Every preset shadow recipe MUST be consistent with this
single direction — no per-element light angles (elevation.kmd § R5).
R2 — Shadow anatomy
A cast shadow is two layers, both tokenized:
| Layer | Purpose | Driven by |
|---|---|---|
| Umbra (key shadow) | Tight, darker contact shadow | Depth level + key-light hardness |
| Penumbra (ambient shadow) | Wide, soft diffusion | Depth level + ambient intensity |
This is exactly why elevation.kmd recipes are two box-shadow
layers (a tight one + a wide one). Higher depth = larger offset +
larger blur on both layers (object is farther from the surface it
casts onto).
R3 — Softness tokens
| Token | Meaning | Default |
|---|---|---|
--kds-light-azimuth | Horizontal light angle (deg) | -30 |
--kds-light-elevation | Vertical light angle (deg) | 60 |
--kds-light-hardness | Umbra sharpness 0(soft)–1(hard) | 0.4 |
--kds-ambient-intensity | Penumbra strength 0–1 | 0.4 |
Presets re-author the rig, not individual shadows:
| Preset | Rig change |
|---|---|
verge (default) | Canonical rig above |
macos_sonoma | Softer: hardness 0.2, longer penumbra |
material_expressive | Stronger key: hardness 0.6 |
windows_95 / brutalist / terminal_classic | Light rig off — bevel/flat per elevation.kmd § R4 |
neumorphism | Dual light (key + opposite fill) for inset/extrude |
R4 — Dark mode is re-lighting, not dimming
The common mistake: in dark mode, keep the same shadow but lower its opacity. Wrong — on a dark surface a darker shadow is invisible.
Koder dark mode re-lights:
- Key shadow stays (darker, larger blur —
elevation.kmd § R2dark recipe) but contributes little. - Tonal overlay becomes the primary depth cue: the accent-tinted
surface lightens with depth (the object catches more key light the
closer it is). This is the
--tonal-*system inelevation.kmd.
So: light mode = shadow-driven, dark mode = light-catch-driven (tonal lift). Same physical model, opposite dominant cue.
R5 — 3D & XR cast shadows
For real-geometry surfaces (components/model-viewer.kmd, XR panels):
- The same rig (R1 azimuth/elevation) drives the renderer's directional light — a glTF mesh and a CSS card are lit from the same angle.
- XR shadow distance (
--kds-xr-shadow-distance,xr-preview.kmd § R3) is the spatial projection of--kds-light-elevation+ depth level. - Contact shadow under a floating XR panel uses the umbra; the soft ground shadow uses the penumbra.
R6 — Forbidden patterns
- ❌ Per-element light direction (breaks the single-rig illusion).
- ❌ Shadows that fall up (light from below — uncanny except
deliberate
neumorphisminset). - ❌ Colored shadows other than the system's near-black/tonal pair
(
elevation.kmd § R5). - ❌ Dark-mode shadow that only dims opacity with no tonal lift (R4).
- ❌ Hard-coded light angles in a renderer; always read the rig tokens.
R7 — Accessibility & forced colors
- Light/shadow is never the sole signal of state or depth
(
elevation.kmd § R7). - Under
forced-colors: active, the rig is inert — depth falls back to borders (elevation.kmd § R6). prefers-reduced-transparencydoes not affect lighting (it affectsmaterials.kmd), but reduced-motion freezes any animated light (e.g. drag-follow highlights).
T1-T3 — Tests
T1 — Single direction: across all canonical presets, every resolved
--shadow-* recipe casts in the R1 direction (offset-y > 0, |offset-x|
consistent sign). koder-spec-audit lighting --direction.
T2 — Dark re-light: in dark mode each depth level resolves a non-zero
--tonal-* overlay (proves light-catch cue exists, not just dimming).
T3 — Rig coherence: a model-viewer directional light angle equals
--kds-light-azimuth/elevation within 1°.
Cross-link
themes/elevation.kmd— the 2D rendered output of this modelthemes/depth.kmd— the abstract z-axis this model illuminatesthemes/light-dark.kmd— light/dark cue balance (R4)components/model-viewer.kmd— real-geometry lighting (R5)develop/xr-preview.kmd— spatial shadow projection (R5)
Referências
specs/themes/elevation.kmdspecs/themes/depth.kmdspecs/themes/light-dark.kmdspecs/themes/ui-style.kmdspecs/develop/xr-preview.kmd