Context
Wave 2 introduces ASCII projection of the Lumos voxel orb. Each projected cell carries an OKLab color sampled from its source voxel, but terminals consume ANSI escape sequences — either the 256-color palette (\u001B[38;5;{n}m) or truecolor (\u001B[38;2;{r};{g};{b}m). This ticket provides the primitive that bridges those two worlds.
phosphor-core already contains CognitiveColorRamp, a luminance-driven ANSI ramping helper. AnsiColorMap is a complementary primitive that takes a full OKLab color (not a scalar luminance) and produces a terminal-ready escape sequence. Both belong in phosphor-core — ANSI is a wire format, not a UI framework, so it doesn't violate the pure-core principle.
Objective
A pure-function OKLab → ANSI escape sequence primitive supporting both 256-color and truecolor modes.
Expected Outcomes
AnsiColorMode enum with ANSI_256 and TRUECOLOR members
AnsiColorMap object with:
escape(color: OklabColor, mode: AnsiColorMode): String — returns the foreground escape sequence
backgroundEscape(color: OklabColor, mode: AnsiColorMode): String — returns the background escape sequence
nearestPaletteIndex(color: OklabColor): Int — exposed for testing and for callers that want the raw palette index
- Quantization uses the standard 256-color palette structure: 16 base colors (indices 0–15), 6×6×6 RGB cube (16–231), 24-step grayscale ramp (232–255). Nearest-color search goes OKLab → linear sRGB → sRGB → cube lookup.
- Unit tests covering: pure red/green/blue snap to expected cube indices, near-grayscale snaps to grayscale ramp not cube, truecolor mode emits raw 24-bit values.
Technical Constraints
- Lives in
phosphor-core at src/commonMain/kotlin/link/socket/phosphor/color/AnsiColorMap.kt.
- Uses Wave 0's
OklabColor and LinearRgbColor from color/OklabColor.kt. No new color types.
- Pure Kotlin, no platform-specific code.
- No allocations in the hot path — the 256-color cube lookup table is computed once at object init.
API Surface Verification (before starting)
Confirm in socket-link/phosphor:
OklabColor and LinearRgbColor data class definitions and the conversion path between them.
- Whether
CognitiveColorRamp already exposes any escape-sequence helpers worth reusing (likely not — it's luminance-only — but worth verifying so we don't duplicate).
Tasks
- Implement linear sRGB → sRGB gamma encoding helper (if not already present in OklabColor.kt). Validate: round-trip
linearToSrgb(srgbToLinear(0.5)) ≈ 0.5.
- Implement 256-color palette table generation (lazy
val initialized once). Validate: table has exactly 256 entries, index 16 maps to RGB (0,0,0), index 231 maps to RGB (255,255,255).
- Implement
nearestPaletteIndex. Validate: pure red OKLab snaps to palette index in the red region of the cube (e.g., index 196 or similar — verify exact expected value with a one-off test).
- Implement
escape and backgroundEscape for both modes. Validate: pure red truecolor returns exactly "\u001B[38;2;255;0;0m"; pure red ANSI_256 returns "\u001B[38;5;{expected_index}m".
- Add unit tests covering edge cases: pure white, pure black, mid-gray, near-saturated red/green/blue/purple/amber (matches Lumos atmosphere palette).
Out of Scope
- Background-color compositing logic.
LumosTerminalFrame.TerminalCell.background is set elsewhere (sibling ticket on LumosTerminalFrame DTO).
- Differential update / escape sequence diffing across frames. Lives in the
CliOrb renderer ticket.
- Color cycling animations. Atmospheres handle that upstream.
- Windows-specific palette quirks (legacy 16-color mode). Truecolor is required on any host running Lumos.
Context
Wave 2 introduces ASCII projection of the Lumos voxel orb. Each projected cell carries an OKLab color sampled from its source voxel, but terminals consume ANSI escape sequences — either the 256-color palette (
\u001B[38;5;{n}m) or truecolor (\u001B[38;2;{r};{g};{b}m). This ticket provides the primitive that bridges those two worlds.phosphor-corealready containsCognitiveColorRamp, a luminance-driven ANSI ramping helper.AnsiColorMapis a complementary primitive that takes a full OKLab color (not a scalar luminance) and produces a terminal-ready escape sequence. Both belong inphosphor-core— ANSI is a wire format, not a UI framework, so it doesn't violate the pure-core principle.Objective
A pure-function OKLab → ANSI escape sequence primitive supporting both 256-color and truecolor modes.
Expected Outcomes
AnsiColorModeenum withANSI_256andTRUECOLORmembersAnsiColorMapobject with:escape(color: OklabColor, mode: AnsiColorMode): String— returns the foreground escape sequencebackgroundEscape(color: OklabColor, mode: AnsiColorMode): String— returns the background escape sequencenearestPaletteIndex(color: OklabColor): Int— exposed for testing and for callers that want the raw palette indexTechnical Constraints
phosphor-coreatsrc/commonMain/kotlin/link/socket/phosphor/color/AnsiColorMap.kt.OklabColorandLinearRgbColorfromcolor/OklabColor.kt. No new color types.API Surface Verification (before starting)
Confirm in
socket-link/phosphor:OklabColorandLinearRgbColordata class definitions and the conversion path between them.CognitiveColorRampalready exposes any escape-sequence helpers worth reusing (likely not — it's luminance-only — but worth verifying so we don't duplicate).Tasks
linearToSrgb(srgbToLinear(0.5)) ≈ 0.5.valinitialized once). Validate: table has exactly 256 entries, index 16 maps to RGB (0,0,0), index 231 maps to RGB (255,255,255).nearestPaletteIndex. Validate: pure red OKLab snaps to palette index in the red region of the cube (e.g., index 196 or similar — verify exact expected value with a one-off test).escapeandbackgroundEscapefor both modes. Validate: pure red truecolor returns exactly"\u001B[38;2;255;0;0m"; pure red ANSI_256 returns"\u001B[38;5;{expected_index}m".Out of Scope
LumosTerminalFrame.TerminalCell.backgroundis set elsewhere (sibling ticket onLumosTerminalFrameDTO).CliOrbrenderer ticket.