Conversation
Removed duplicate logo image from README.
Phase 0 (make it load): close 35 unclosed object literals, fix hwb duplicate import + hcy missing export. Suite runs again: 111 pass, 1 skip. Correctness (Opus audit, all findings adversarially confirmed; each fix verified vs colorjs.io/culori with a regression test): - oklab: linearize sRGB before the LMS matrix (reuse lrgb transfer); fixes oklab/oklch/okhsl/okhsv -- gray L 79.47 -> 59.99 (matches colorjs 2e-6) - lab-d50: fix 100x scale (white L 522 -> 100); matches colorjs D50 lab - wiring: bracket-hyphenated hub keys -> 7 spaces reachable from rgb, pinned by an integrity/reachability test Docs reconciled to truth (71 spaces, not 72); full verified defect inventory + tiered plan in docs/todo.md.
All from the Opus audit, each proven against a reference / first principles and pinned by a regression test: - yuv: inverse blue coeff 2.02311 -> 2.03211 = (1-Kb)/Umax (digit transposition) - jzczhz: drop the double x100 (jzazbz already conventional); pure polar transform, roundtrip 3e-15, hue matches colorjs - tsl: inverse recovers theta=atan2(g',r') via cos/sin (sign preserved) + black guard; exact roundtrip (red was [124,83,-83]) - hcl: inverse uses frac() not JS % (Chilliant formula); full 360-hue sweep roundtrips with 0 failures (green was [255,255,0]) - labh: D65 white-point ratios instead of Illuminant-C; white -> [100,0,0] (was a=-5.34)
hcy was a verbatim copy of hsi (Y = arithmetic mean). Replaced with the Chilliant RGB-HCY model: Y is the color's Rec.601 luma (red 29.9, green 58.7, blue 11.4 -- was 33.3 for all), chroma renormalized by the hue's luma so equal Y means equal brightness. Achromatic inputs are NaN-safe; exact roundtrip. Verified + regression test.
- hwb: white + black ≥ 100 now returns an exact gray (CSS Color 4), killing the ~1e-14 inter-channel residue that made neighbouring hue sliders jump as whiteness was dragged; pinned by a regression test - docs: docs/landing/ modules moved to docs/js/; removed the unused engine.js and the scratch study pages - sliders: channel value centres vertically and slides aside only when the picker actually reaches it; picker shows whole at the min/max edges; bar labels adopt the plot axis-label style - dossier: one shared tag recipe for the hero label, title pills and gamut chips; transfer curve extended to perceptual spaces with a live luminance marker; reach shown as % of visible; historical spaces no longer faded
… pure WGSL translate, ~4 kB bundles vs the ~200 kB catalog
…gsl beside it, bare specifiers
…runtime-chosen spaces
The shipped Hunt inverse ran a plain Newton in XYZ and diverged for saturated colours — a purple round-tripped with error ~19. Root cause: the cone nonlinearity f_n(x)=40x^0.73/(x^0.73+2) has an infinite slope at x=0, so the XYZ->(J,C,h) Jacobian is singular near black; the Newton lurched to J<0 on iteration 0 and bailed. Fix: invert in ADAPTED-CONE space (r_a,g_a,b_a) where black is the regular point (1,1,1), not the origin singularity, and use Levenberg-Marquardt (damping handles the still-ill-scaled chroma directions). XYZ is recovered analytically: the per-channel f_n inverse gives cone signals, HPE^-1 gives XYZ. Forward refactored to share a correlate(rgbA,Y) helper; forward math and the colour-science doctest are byte-identical. Result over the full sRGB cube: machine-exact (<6e-12) for every colour with lightness J>=1 (was: purple err 19). Colours below J=1 (essentially black, <1% lightness) sit in a residual landscape whose true preimage is well-conditioned but has a microscopic basin (0/400 random seeds hit it) — they return a bounded near-black value; documented as a known limitation. GL chunk mirrors the new inverse exactly (f64-eval matches scalar to 3e-15); declares tol 2e-2 for the near-black f32 GPU path.
… at -19) The space output Nayatani's L*_P achromatic lightness, which is anchored at grey=50 and puts black at -19 and white at 113 — so its declared range started at -20 and a colour-picker plane wasted ~15% of the lightness axis on unusable sub-zero black. L*_P is a legitimate correlate but a poor lightness axis. Switched the lightness channel to Nayatani's NORMALISED achromatic lightness L*_N = 100·B_r/B_rw (white -> 100.0, black -> ~0), the model's own [0,100]-style lightness, using the exact ideal-white-brightness formula (colour-science). L*_P = Q+50 is still computed internally as the chroma anchor ((L*_P/50)^0.7·S), so chroma is unchanged. The switch is an affine remap of Q, so the exact 4-regime inverse is untouched apart from recovering L*_P from L*_N first; round-trip stays 1e-13. Re-pinned the bonafide value to the doctest's L*_N=50.0039154 (was L*_P=49.9998829 — both are colour-science XYZ_to_Nayatani95 outputs for the same input). Range 0-100; sRGB now spans -5.1..99.2 (the -5 near-black dip documented). GL chunk mirrors it; meta/types regenerated.
…for Hunt purple) The NaN canary only checks finiteness, so the Hunt XYZ-Newton returning near-grey for a saturated purple (round-trip error ~19) passed silently. Add a round-trip accuracy test over hunt/nayatani95/llab/atd95/rlab/osaucs (incl. the exact purple XYZ that broke) and ryb (its Newton once returned white for saturated red). Guards the whole numerically-inverted class going forward.
…cted, glsl(oklch) both ways, glsl(oklch, p3) cross-space; excluded/no-path errors name their reason; wasm docs state hub-only rationale and real ~30 kB size; gamut panel viewport tuned
…spaces left cubehelix was the last forward-only space: rgb.cubehelix threw because a 1-parameter colormap can't invert an arbitrary RGB. Now rgb.cubehelix returns the nearest point on the helix — a coarse scan + golden-section refine over the fraction. Colours on the helix recover their fraction exactly (cubehelix->rgb->cubehelix round-trips to 8e-16); off-helix colours project onto it, the way conversions into rgb clamp to the sRGB gamut. Custom forward params can't be recovered from one colour, so the inverse assumes defaults. GL chunk gains the matching inverse edge (scalar math per the chunk contract). Dropped cubehelix from the test's skip sets — every one of the 156 spaces is now bidirectional.
Two new per-space metadata axes the catalog lacked: @method (how a space is computed — transfer/matrix/opponent/cylindrical/luma-chroma/chromaticity/appearance/system/spectral) and @encoding (value domain — linear/gamma/log/pq/hlg/perceptual/chromaticity). Answers 'what kind of thing is this space' and the earlier 'does it have a transfer function' question, and makes the catalog filterable/groupable by method. All 156 classified and covered; generate-meta parses the tags; meta/types regenerated. README meta example updated.
New gamuts.js (like whitepoints.js) holds the primary chromaticities + white for 13 standard RGB gamuts (sRGB, Display-P3, DCI-P3, Rec.2020, Adobe RGB, ProPhoto, AP0, AP1, Apple RGB, NTSC, PAL, SMPTE-C, CIE RGB), values from CSS Color 4 / the defining specs. 25 RGB working spaces carry a one-word @gamut tag; generate-meta resolves it to meta.gamut + meta.primaries {r,g,b} + meta.white. The D65 gamuts' primaries are pinned by a test against each space's live R/G/B->XYZ chromaticities (all match); D50/D60 (ProPhoto/ACES) use cited native-white primaries since the D65 hub adapts them. README meta example updated.
…al now The explorer's forward-only fallback (locally explored params when rgb->space throws) no longer triggers: cubehelix, its one example, inverts by nearest-fraction projection since 79c4c2a. Comments now describe the mechanism as the safety net it is, and the 'one-way' pill tooltip's direction is corrected (such a space converts TO rgb only — the old text said 'from RGB only', backwards). Behavior verified: valsFor(cubehelix) returns the projected fraction, sliders drive master, pill reads 'bidirectional'. Mechanism kept as a guard.
Every space is bidirectional now, so the explorer's forward-only pathway is deleted rather than kept as a dead safety net: the probeVals/localVals locally-explored-params fallback, the one-way facet + pill + tooltip, the fmt() 'forward-only' placeholder, and the fallback branches in the card/dossier slider handlers (adopt() cannot fail). The dossier code snippet now always shows both directions — for cubehelix it renders rgb.cubehelix(...) -> [fraction] live. New 'channels' facet in the tag picker: chips 1/2/3/4 filter the catalog by component count (1 = gray/kelvin/wavelength/cubehelix, 4 = cmyk), OR within the facet, AND against the others, sorted numerically. Verified in-browser (served no-store, fresh profile): zero console errors, channels chips filter and clear correctly, cubehelix dossier opens with working fraction slider driving the master color, hash carries #cubehelix@fraction. Rides along: in-flight hairline-divider and modal-backdrop styling refinements.
…ces get the panel The dossier's transfer plot was gated on a proxy (archetype + tone channel at index 0), which silently skipped munsell, coloroid, ral-design, ostwald, cmyk, cubehelix, xyb, tsl, the YCbCr/luma family, xyy and every device cylinder. Now every non-hue channel is sampled along the grey axis and the monotonic responder over >5% of its range is the tone carrier: V for Munsell, K (falling) for CMYK, W for Ostwald/HWB, luma for YCbCr, fraction for cubehelix. The classified tone channel wins ties; hue drift on neutrals is noise and skipped. rg, kelvin and wavelength stay panel-less — the chromaticity/CCT of grey is a constant. The 'perceptual lightness' caption is now verified, not asserted: a perceptual encoding AND a measured 18%-grey lift — Ostwald's W is linear on greys, so it reads plain 'code value'. Endpoint labels snap float dust (K's 1e-14) to 0. All 120 previously-paneled spaces keep byte-identical curves (differentially verified old rule vs new across the full registry).
…aved buffers, all 156 spaces, pinned bit-identical to scalar; color-space/lite: the 27 wasm-covered spaces as a compact JS hub; wasm kernels as true multi-value exports
…emit 1D 4096, cross-channel 3D 17/33/65; header carries measured median/max deviation; differential suite + ffmpeg end-to-end check
…ows the live registry, not a deny-list — leakage test-pinned
…er, terminal-black code; mobile categories fold with animation, all names small; rail titles align with leads and clear the end divider; slider letters take ink from where they land; hue strip sheds its marker; masonry keeps catalog order across resizes
…nge claims corrected; migration guide verified against v2.3.2 source
…d site - lut.js: any pair as a .cube LUT — auto-1D for pure transfers, self-verifying header (measured median/max deviation vs the direct conversion), shaper option (Resolve 1D+3D combined: shaped 33³ beats plain 65³ for log→display); format pinned to Adobe spec + ffmpeg end-to-end test - icc.js: RGB working spaces as ICC v2 matrix+TRC display profiles, derived mechanically from the conversions (Bradford→D50 PCS, diagonal-sampled TRC, empirical matrix×transfer gate); pinned to Lindbloom/IEC, ColorSync-verified - mcp.js: zero-dep MCP stdio server — convert/space/spaces/cube tools for agents - data.json: the whole registry as one language-neutral artifact (metadata, graph edges, gamuts, whitepoints, CIE 1931 CMFs, 129 cited conformance triples single-sourced from test/refs.js); meta.js folded into it - spaces/: the 156 space modules move out of the root; public import paths unchanged via the exports map (wildcard → spaces/, exact keys for root) - source-only repo: types/dist/wasm-binary/data generated by `prepare`; website stages into _site (build-site.js) and deploys via pages.yml with the suite as deploy gate; docs/ holds sources only; llms/sitemap/robots + 156 per-space reference pages generated - site: /<name> deep links (404 bounce + ?s= normalization), export facet, dossier LUT block with live measured deviation, lut API tab - planning docs → .work/
index.html, 404, js/, img/, tokens.css → web/ (staged into _site as before); build-site/generate-landing/generate-reach/colorsync-test repointed. README's docs/*.md links unchanged.
…ive purples xyz.wavelength returned dsh's signed dominant wavelength raw, so purples came back negative (xyy(0.46, 0.03) → -519) — outside the declared 380-700 range — and the forward then clamped them to 380, rendering the opposite hue. Purples, which have no dominant wavelength of their own, now snap to the nearer end of the line of purples via spectralWavelength (exported from dsh.js beside the signed dominantWavelength, which DSH keeps for its d channel, per CIE 15). Spectral-arc hits below 380 nm clamp into range too. The achromatic 0 sentinel now renders as the D65 neutral instead of 380 nm violet — mirroring dsh's d=0 — in JS and the GL chunk both. Whole-rgb-cube regression test pins the domain; the CIE 018:2019 forward pin at 550 nm and dsh's colour-science pins are untouched.
…he input's baseline, chevrons spin with hold-repeat, copied rows paste back; precision from range span, printed = step, dossier unified; rhombus is a native picker, favicon-sized, value input fits any CSS color; rail titles push off like native sticky; hairlines start at the spaces; 3 cols from 1080; inputs field-size to content
At a non-physical coordinate (xyy y→0 with fixed Y explodes past the linear-light ceiling) the GPU bar and plane sweeps void to the checkerboard, but the bar knob and plane crosshair still painted the clamped master hex — a blazing magenta marker on a voided-dark sweep. The markers now check the same lens-independent physicality rule the shaders void by (±4× diffuse white SDR, ×500 scene/HDR) and render hollow — position only, no color claim — the vocabulary the CIE plate's marker already uses for beyond-MacAdam coordinates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Targets the v3 milestone.
Resolves the open-issue backlog on merge. Each issue has a status comment; they auto-close when this lands on
master.Fixed in v3 — closes #55, closes #54, closes #53, closes #51, closes #48, closes #47, closes #42, closes #40, closes #39, closes #38, closes #36, closes #33, closes #32, closes #28, closes #7
Declined-with-reason (proprietary catalogs / out-of-scope notation / alpha-not-a-channel — all documented in the README) — closes #17, closes #16, closes #45, closes #41
Supersedes #68 — its hcg negative-hue fix is already in v3 (
hcg.js); the contributor's test assertion was ported with credit.Left open: #6 (WebGL/WebGPU shader exports) — a genuine future direction tracked in
docs/todo.md.