Enhancement : 3D Gamut Visualisation Rework — Axes, Reference Rendering, Per-Vertex Colour & Controls #185

Closed
opened 2026-09-02 09:09:18 +01:00 by gronod · 4 comments
Owner

Problem

The 3D gamut viewer in Stage 5 (Verify Profile) has three significant usability problems that undermine its value as a diagnostic tool:

1. Unlabelled, misleading axes

The viewer uses a generic THREE.AxesHelper(100) which draws three coloured lines (Red, Green, Blue) starting at the origin. There are no text labels (L*, a*, b*) and the RGB line colours misleadingly suggest an RGB coordinate space rather than CIELAB. Without labels or tick marks, a user cannot interpret scale or orientation.

2. Messy, unlabelled sRGB reference wireframe

The sRGB reference gamut is rendered using MeshLambertMaterial({ wireframe: true }) on the raw triangulated mesh. This draws every internal triangle edge of the 892-face mesh, creating a dense, cluttered cage. It has no legend and no way to toggle it on/off. Users cannot tell what it represents.

3. No interactive controls or legend

There is no UI overlay to identify the two shapes (profile vs. reference), no toggle to isolate layers, and no ability to switch rendering modes. The profile gamut itself is rendered as a flat uniform blue (0x3b82f6) rather than per-vertex true colour.


Screenshot (Current State)

Screenshot 2026-09-02 090657.png


Industry Context

Professional gamut visualisation tools (macOS ColorSync Utility, GamutVision, ICC Examin, Chromix ColorThink) follow consistent patterns:

  • Labelled bounding volume: Graduated axes with tick marks spanning the CIELAB domain (L*: 0→100, a*/b*: ±128).
  • Distinct rendering: Reference gamut as a clean structural wireframe (only hard edges, not internal triangulation) or faint transparent solid. Profile gamut as a solid with per-vertex true-colour shading.
  • Interactive controls: Toggle switches for visibility of each layer, rendering mode, and opacity.

Proposed Solution (6 Components)

Component 1: Custom CIELAB Axis Scaffold with CSS2D Labels

  • Remove the generic THREE.AxesHelper and THREE.GridHelper.
  • Draw three axis lines as THREE.Line with correct CIELAB ranges (L*: 0→100, a*/b*: −128→+128).
  • Add tick marks at key intervals (L* at 0/25/50/75/100, a*/b* at −128/−64/0/+64/+128).
  • Add a subtle bounding wire box (EdgesGeometry(BoxGeometry)) framing the full CIELAB domain.
  • Add a correctly scaled ground plane grid at L*=0.
  • Use CSS2DRenderer (new ~5KB addon) for crisp text labels: "L*", "a*", "b*", and colour-science indicators ("+a* (Red)", "−a* (Green)", "+b* (Yellow)", "−b* (Blue)").

Component 2: Clean sRGB Reference Gamut

  • Replace the wireframe render with THREE.EdgesGeometry(geometry, 15) rendered via LineSegments — this strips out all co-planar internal edges, leaving only the structural outline.
  • Add a faint transparent solid fill underneath (opacity ~0.08, depthWrite: false) to give the reference subtle physical presence.
  • Both objects in a THREE.Group for easy toggle control.

Component 3: Per-Vertex True-Colour Profile Gamut

  • Replace the flat blue MeshLambertMaterial({ color: 0x3b82f6 }) with per-vertex colouring.
  • For each vertex, compute labToSrgb(L, a, b) using the existing color_convert.js utility and store in a vertex colour buffer.
  • Use MeshLambertMaterial({ vertexColors: true, opacity: 0.85 }).
  • Result: the profile gamut blob shows its actual colours — reds are red, blues are blue, etc.

Component 4: Interactive Legend & Controls Overlay

  • Add a glassmorphic floating panel (position: absolute) over the top-right corner of the viewer.
  • Three rows: "Profile Gamut" / "sRGB Reference" / "CIELAB Axes", each with a colour swatch and a toggle switch.
  • Toggles control group.visible for each layer.

Component 5: CSS Styling

  • Add dedicated gamut viewer styles: panel backdrop-filter, toggle switches matching the app's dark theme, CSS2D label typography.

Component 6: Module Wiring

  • Export toggle functions from gamut_viewer.js.
  • Wire checkboxes to toggle functions.
  • Add CSS2DRenderer.js script tag to index.html.
  • Add labelRenderer.render() to the animation loop.

Files Affected

File Action Description
src/js/gamut_viewer.js MODIFY Major rework: axis scaffold, CSS2D labels, EdgesGeometry sRGB reference, per-vertex colour profile, toggle exports
src/index.html MODIFY Add CSS2DRenderer script tag; add legend/controls overlay HTML in Stage 5
src/styles/main.css MODIFY Add gamut viewer section, controls panel, toggle switch, and CSS2D label styles
src/js/CSS2DRenderer.js NEW Three.js CSS2DRenderer addon (~5KB vendor file)
## Problem The 3D gamut viewer in Stage 5 (Verify Profile) has three significant usability problems that undermine its value as a diagnostic tool: ### 1. Unlabelled, misleading axes The viewer uses a generic `THREE.AxesHelper(100)` which draws three coloured lines (Red, Green, Blue) starting at the origin. There are no text labels (L*, a*, b*) and the RGB line colours misleadingly suggest an RGB coordinate space rather than CIELAB. Without labels or tick marks, a user cannot interpret scale or orientation. ### 2. Messy, unlabelled sRGB reference wireframe The sRGB reference gamut is rendered using `MeshLambertMaterial({ wireframe: true })` on the raw triangulated mesh. This draws **every internal triangle edge** of the 892-face mesh, creating a dense, cluttered cage. It has no legend and no way to toggle it on/off. Users cannot tell what it represents. ### 3. No interactive controls or legend There is no UI overlay to identify the two shapes (profile vs. reference), no toggle to isolate layers, and no ability to switch rendering modes. The profile gamut itself is rendered as a flat uniform blue (`0x3b82f6`) rather than per-vertex true colour. --- ## Screenshot (Current State) [Screenshot 2026-09-02 090657.png](https://git.i3omb.com/attachments/ebabeae4-f799-4477-af3d-ab37ac013c80) --- ## Industry Context Professional gamut visualisation tools (macOS ColorSync Utility, GamutVision, ICC Examin, Chromix ColorThink) follow consistent patterns: - **Labelled bounding volume:** Graduated axes with tick marks spanning the CIELAB domain (L*: 0→100, a*/b*: ±128). - **Distinct rendering:** Reference gamut as a clean structural wireframe (only hard edges, not internal triangulation) or faint transparent solid. Profile gamut as a solid with per-vertex true-colour shading. - **Interactive controls:** Toggle switches for visibility of each layer, rendering mode, and opacity. --- ## Proposed Solution (6 Components) ### Component 1: Custom CIELAB Axis Scaffold with CSS2D Labels - Remove the generic `THREE.AxesHelper` and `THREE.GridHelper`. - Draw three axis lines as `THREE.Line` with correct CIELAB ranges (L*: 0→100, a*/b*: −128→+128). - Add tick marks at key intervals (L* at 0/25/50/75/100, a*/b* at −128/−64/0/+64/+128). - Add a subtle bounding wire box (`EdgesGeometry(BoxGeometry)`) framing the full CIELAB domain. - Add a correctly scaled ground plane grid at L*=0. - Use `CSS2DRenderer` (new ~5KB addon) for crisp text labels: "L*", "a*", "b*", and colour-science indicators ("+a* (Red)", "−a* (Green)", "+b* (Yellow)", "−b* (Blue)"). ### Component 2: Clean sRGB Reference Gamut - Replace the wireframe render with `THREE.EdgesGeometry(geometry, 15)` rendered via `LineSegments` — this strips out all co-planar internal edges, leaving only the structural outline. - Add a faint transparent solid fill underneath (opacity ~0.08, `depthWrite: false`) to give the reference subtle physical presence. - Both objects in a `THREE.Group` for easy toggle control. ### Component 3: Per-Vertex True-Colour Profile Gamut - Replace the flat blue `MeshLambertMaterial({ color: 0x3b82f6 })` with per-vertex colouring. - For each vertex, compute `labToSrgb(L, a, b)` using the existing `color_convert.js` utility and store in a vertex colour buffer. - Use `MeshLambertMaterial({ vertexColors: true, opacity: 0.85 })`. - Result: the profile gamut blob shows its actual colours — reds are red, blues are blue, etc. ### Component 4: Interactive Legend & Controls Overlay - Add a glassmorphic floating panel (`position: absolute`) over the top-right corner of the viewer. - Three rows: "Profile Gamut" / "sRGB Reference" / "CIELAB Axes", each with a colour swatch and a toggle switch. - Toggles control `group.visible` for each layer. ### Component 5: CSS Styling - Add dedicated gamut viewer styles: panel backdrop-filter, toggle switches matching the app's dark theme, CSS2D label typography. ### Component 6: Module Wiring - Export toggle functions from `gamut_viewer.js`. - Wire checkboxes to toggle functions. - Add `CSS2DRenderer.js` script tag to `index.html`. - Add `labelRenderer.render()` to the animation loop. --- ## Files Affected | File | Action | Description | |:-----|:-------|:------------| | `src/js/gamut_viewer.js` | MODIFY | Major rework: axis scaffold, CSS2D labels, EdgesGeometry sRGB reference, per-vertex colour profile, toggle exports | | `src/index.html` | MODIFY | Add CSS2DRenderer script tag; add legend/controls overlay HTML in Stage 5 | | `src/styles/main.css` | MODIFY | Add gamut viewer section, controls panel, toggle switch, and CSS2D label styles | | `src/js/CSS2DRenderer.js` | NEW | Three.js CSS2DRenderer addon (~5KB vendor file) |
gronod added the Kind/Enhancement
Reviewed
Confirmed
1
Priority
Medium
3
labels 2026-09-02 09:09:18 +01:00
Author
Owner

Findings Investigation

After reviewing the codebase and the current gamut viewer implementation (src/js/gamut_viewer.js), here are my findings:

  1. Missing Axes Labels: The current implementation uses a generic Three.js AxesHelper which draws Red, Green, and Blue lines mapped to the a*, L*, and b* axes, respectively. There are no textual labels (like L*, a*, b*) which makes it non-intuitive to read.
  2. Unlabeled sRGB Wireframe: The wireframe in the background is indeed the sRGB reference gamut. It is loaded automatically on startup but lacks any UI legend or toggle to identify it or turn it off.
  3. "Messy" Wireframe Appearance: The wireframe is currently drawn using raw triangulated mesh lines. This means every internal polygon edge is drawn, resulting in a cluttered and messy look, rather than a clean hull.

Better Approaches & External Ideas

Looking at industry standards like macOS ColorSync Utility and GamutVision:

  • Clean Axes: They typically provide a bounding box or grid with tick marks for the CIELAB space (-128 to 128 for a/b, 0 to 100 for L) rather than generic RGB infinite lines.
  • Comparison Visualization: When comparing gamuts, it is common to have one as an opaque solid and the other as a highly transparent solid (or point cloud), as overlapping wireframes become visually indecipherable.
  • Legends & Controls: Clear legends are always present to identify the reference vs. the subject, often with toggles to isolate views.

Proposed Options

1. Axes Presentation:

  • Option A: Add floating 3D text labels (L*, a*, b*) to the existing lines.
  • Option B (Recommended): Implement a bounding CIELAB grid box with clear tick marks, mimicking a true 3D graph appearance.

2. Reference Gamut (sRGB) Appearance:

  • Option A (Recommended): Change the reference from a wireframe to a faint, semi-transparent solid color.
  • Option B: Use THREE.EdgesGeometry to clean up the wireframe by removing coplanar inner triangles, leaving only the distinct outer hull edges.

3. UI Improvements (Highly Recommended):

  • Add a legend in the UI indicating what each shape/color represents.
  • Add a toggle control to show or hide the reference sRGB gamut.

I will also create an implementation plan in the current workspace based on these options for review.

### Findings Investigation After reviewing the codebase and the current gamut viewer implementation (`src/js/gamut_viewer.js`), here are my findings: 1. **Missing Axes Labels:** The current implementation uses a generic Three.js `AxesHelper` which draws Red, Green, and Blue lines mapped to the `a*`, `L*`, and `b*` axes, respectively. There are no textual labels (like L*, a*, b*) which makes it non-intuitive to read. 2. **Unlabeled sRGB Wireframe:** The wireframe in the background is indeed the sRGB reference gamut. It is loaded automatically on startup but lacks any UI legend or toggle to identify it or turn it off. 3. **"Messy" Wireframe Appearance:** The wireframe is currently drawn using raw triangulated mesh lines. This means every internal polygon edge is drawn, resulting in a cluttered and messy look, rather than a clean hull. ### Better Approaches & External Ideas Looking at industry standards like macOS ColorSync Utility and GamutVision: - **Clean Axes:** They typically provide a bounding box or grid with tick marks for the CIELAB space (-128 to 128 for a/b, 0 to 100 for L) rather than generic RGB infinite lines. - **Comparison Visualization:** When comparing gamuts, it is common to have one as an opaque solid and the other as a highly transparent solid (or point cloud), as overlapping wireframes become visually indecipherable. - **Legends & Controls:** Clear legends are always present to identify the reference vs. the subject, often with toggles to isolate views. ### Proposed Options **1. Axes Presentation:** - **Option A:** Add floating 3D text labels (L*, a*, b*) to the existing lines. - **Option B (Recommended):** Implement a bounding CIELAB grid box with clear tick marks, mimicking a true 3D graph appearance. **2. Reference Gamut (sRGB) Appearance:** - **Option A (Recommended):** Change the reference from a wireframe to a faint, semi-transparent solid color. - **Option B:** Use `THREE.EdgesGeometry` to clean up the wireframe by removing coplanar inner triangles, leaving only the distinct outer hull edges. **3. UI Improvements (Highly Recommended):** - Add a legend in the UI indicating what each shape/color represents. - Add a toggle control to show or hide the reference sRGB gamut. I will also create an implementation plan in the current workspace based on these options for review.
gronod changed title from Enhancement : 3d Gamut Visualisation is not ideal to Enhancement : 3D Gamut Visualisation Rework — Axes, Reference Rendering, Per-Vertex Colour & Controls 2026-09-02 09:37:09 +01:00
Author
Owner

Updated Analysis & Implementation Plan (Supersedes Previous Comment)

After a thorough code review and external research, the ticket body has been updated with a comprehensive 6-component implementation plan.

Key technical findings from the code review:

  1. Axis system (gamut_viewer.js L42–49): Uses THREE.AxesHelper(100) which draws generic R/G/B lines with no labels, and a GridHelper(200, 20) that doesn't match the CIELAB coordinate range. The coordinate mapping is X=a*, Y=L*, Z=b*.

  2. sRGB reference (gamut_viewer.js L202–212): loadSrgbReferenceGamut() calls renderGamutFromText() with isWireframe=true, which uses MeshLambertMaterial({ wireframe: true, opacity: 0.35 }). The sRGB.gam file contains 448 vertices and 892 triangular faces — all 892×3 = 2676 internal triangle edges are drawn, producing the cluttered appearance.

  3. Profile gamut (gamut_viewer.js L214–224, called from colprof.js L195 and profcheck.js L101): loadGamutMesh() renders with color: 0x3b82f6, isWireframe: false — a flat opaque blue. The existing labToSrgb() function in color_convert.js is never used here, despite being the exact utility needed for per-vertex colouring.

  4. No CSS exists for the gamut viewer section. The #gamutViewerContainer and .gamut-viewer-section have no dedicated styles in main.css.

  5. No CSS2DRenderer is currently bundled. The project uses global <script> loading for Three.js (via three.min.js) and OrbitControls.js (IIFE pattern), so the CSS2DRenderer addon should follow the same pattern.

Recommended approach uses THREE.EdgesGeometry (threshold angle 15°) for a clean reference outline, per-vertex true-colour via the existing labToSrgb(), and CSS2DRenderer for crisp axis labels. Full detail in the updated ticket body above.

### Updated Analysis & Implementation Plan (Supersedes Previous Comment) After a thorough code review and external research, the ticket body has been updated with a comprehensive 6-component implementation plan. **Key technical findings from the code review:** 1. **Axis system** (`gamut_viewer.js` L42–49): Uses `THREE.AxesHelper(100)` which draws generic R/G/B lines with no labels, and a `GridHelper(200, 20)` that doesn't match the CIELAB coordinate range. The coordinate mapping is X=a*, Y=L*, Z=b*. 2. **sRGB reference** (`gamut_viewer.js` L202–212): `loadSrgbReferenceGamut()` calls `renderGamutFromText()` with `isWireframe=true`, which uses `MeshLambertMaterial({ wireframe: true, opacity: 0.35 })`. The sRGB.gam file contains 448 vertices and 892 triangular faces — all 892×3 = 2676 internal triangle edges are drawn, producing the cluttered appearance. 3. **Profile gamut** (`gamut_viewer.js` L214–224, called from `colprof.js` L195 and `profcheck.js` L101): `loadGamutMesh()` renders with `color: 0x3b82f6, isWireframe: false` — a flat opaque blue. The existing `labToSrgb()` function in `color_convert.js` is never used here, despite being the exact utility needed for per-vertex colouring. 4. **No CSS exists** for the gamut viewer section. The `#gamutViewerContainer` and `.gamut-viewer-section` have no dedicated styles in `main.css`. 5. **No CSS2DRenderer** is currently bundled. The project uses global `<script>` loading for Three.js (via `three.min.js`) and `OrbitControls.js` (IIFE pattern), so the CSS2DRenderer addon should follow the same pattern. **Recommended approach** uses `THREE.EdgesGeometry` (threshold angle 15°) for a clean reference outline, per-vertex true-colour via the existing `labToSrgb()`, and `CSS2DRenderer` for crisp axis labels. Full detail in the updated ticket body above.
Author
Owner

Implementation Complete

All 6 components from the implementation plan have been delivered.

Files changed:

File Change
src/js/CSS2DRenderer.js NEW — IIFE-wrapped Three.js CSS2D renderer addon, exposes THREE.CSS2DObject and THREE.CSS2DRenderer as globals
src/js/gamut_viewer.js REWORK — Complete rewrite: custom CIELAB axis scaffold, CSS2D labels, EdgesGeometry sRGB reference, per-vertex Lab→sRGB colour, toggle exports
src/index.html MODIFY — Added CSS2DRenderer.js script tag; replaced minimal gamut section with header, viewer wrap, and glassmorphic legend overlay panel
src/styles/main.css MODIFY — Added 210 lines of dedicated gamut viewer CSS: section layout, viewer container, controls panel, colour swatches, iOS toggle switches, CSS2D label styles

What has changed visually:

  • Axes now draw L*, a*, b* lines with correct CIELAB ranges and tick marks at 25/50/75/100 and ±64/±128. Crisp CSS2D text labels with colour-coded indicators (+a* red, −a* green, +b* yellow, −b* blue)
  • sRGB reference uses EdgesGeometry(geometry, 15°) — only structural hull edges are drawn, no internal triangle edges. A very faint solid fill (opacity 0.07) gives it physical presence
  • Profile gamut is per-vertex coloured via labToSrgb() — the blob naturally shows reds, greens, yellows and blues in the right regions
  • Glassmorphic legend panel (top-right overlay) with three iOS-style toggle switches to independently show/hide Profile Gamut, sRGB Reference, and CIELAB Axes
  • Orbit camera target adjusted to L*=50 (centre of CIELAB space) for better default orientation

Build status: Rust compiled successfully. JS syntax checks pass.

### Implementation Complete All 6 components from the implementation plan have been delivered. **Files changed:** | File | Change | |:-----|:-------| | `src/js/CSS2DRenderer.js` | **NEW** — IIFE-wrapped Three.js CSS2D renderer addon, exposes `THREE.CSS2DObject` and `THREE.CSS2DRenderer` as globals | | `src/js/gamut_viewer.js` | **REWORK** — Complete rewrite: custom CIELAB axis scaffold, CSS2D labels, EdgesGeometry sRGB reference, per-vertex Lab→sRGB colour, toggle exports | | `src/index.html` | **MODIFY** — Added `CSS2DRenderer.js` script tag; replaced minimal gamut section with header, viewer wrap, and glassmorphic legend overlay panel | | `src/styles/main.css` | **MODIFY** — Added 210 lines of dedicated gamut viewer CSS: section layout, viewer container, controls panel, colour swatches, iOS toggle switches, CSS2D label styles | **What has changed visually:** - Axes now draw L*, a*, b* lines with correct CIELAB ranges and tick marks at 25/50/75/100 and ±64/±128. Crisp CSS2D text labels with colour-coded indicators (+a* red, −a* green, +b* yellow, −b* blue) - sRGB reference uses `EdgesGeometry(geometry, 15°)` — only structural hull edges are drawn, no internal triangle edges. A very faint solid fill (opacity 0.07) gives it physical presence - Profile gamut is per-vertex coloured via `labToSrgb()` — the blob naturally shows reds, greens, yellows and blues in the right regions - Glassmorphic legend panel (top-right overlay) with three iOS-style toggle switches to independently show/hide Profile Gamut, sRGB Reference, and CIELAB Axes - Orbit camera target adjusted to L*=50 (centre of CIELAB space) for better default orientation **Build status:** Rust compiled successfully. JS syntax checks pass.
gronod added this to the Milestone 13 : Polish milestone 2026-09-03 22:55:24 +01:00
Author
Owner

Implemented in enh/185-gamut-viewer (stacked on enh/176-colprof-fwa):

  • Added resetCamera() public API and a Reset View button.
  • Added per-layer opacity sliders for the profile gamut, sRGB reference, and CIELAB axes.
  • Added R keyboard shortcut to reset the camera while Stage 5 is active.
  • Added JSDoc to all public gamut_viewer.js functions.
  • Added supporting CSS for the new controls and updated documentation.

Closes #185.

Implemented in `enh/185-gamut-viewer` (stacked on `enh/176-colprof-fwa`): - Added `resetCamera()` public API and a Reset View button. - Added per-layer opacity sliders for the profile gamut, sRGB reference, and CIELAB axes. - Added `R` keyboard shortcut to reset the camera while Stage 5 is active. - Added JSDoc to all public `gamut_viewer.js` functions. - Added supporting CSS for the new controls and updated documentation. Closes #185.
Sign in to join this conversation.