{"id":47683369,"url":"https://github.com/dkryaklin/colordx","last_synced_at":"2026-04-22T19:01:36.287Z","repository":{"id":346173211,"uuid":"1188857768","full_name":"dkryaklin/colordx","owner":"dkryaklin","description":"A high-performance color library with extended support for modern color spaces including OKLCH, OKLAB, Display-P3, and more","archived":false,"fork":false,"pushed_at":"2026-04-02T09:30:27.000Z","size":4045,"stargazers_count":15,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-02T10:27:28.247Z","etag":null,"topics":["cmyk","color","color-conversion","color-manipulation","color-space","colour","display-p3","hex","hsl","hsv","hwb","lab","lch","oklab","oklch","perfomance","rgb","wide-gamut","xyz"],"latest_commit_sha":null,"homepage":"https://colordx.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dkryaklin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-22T17:17:54.000Z","updated_at":"2026-04-02T09:30:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dkryaklin/colordx","commit_stats":null,"previous_names":["dkryaklin/colordx"],"tags_count":33,"template":false,"template_full_name":null,"purl":"pkg:github/dkryaklin/colordx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkryaklin%2Fcolordx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkryaklin%2Fcolordx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkryaklin%2Fcolordx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkryaklin%2Fcolordx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dkryaklin","download_url":"https://codeload.github.com/dkryaklin/colordx/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkryaklin%2Fcolordx/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31307762,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cmyk","color","color-conversion","color-manipulation","color-space","colour","display-p3","hex","hsl","hsv","hwb","lab","lch","oklab","oklch","perfomance","rgb","wide-gamut","xyz"],"created_at":"2026-04-02T14:20:07.824Z","updated_at":"2026-04-22T19:01:36.176Z","avatar_url":"https://github.com/dkryaklin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/logo.svg\" width=\"128\" height=\"128\" alt=\"colordx\" /\u003e\n\u003c/p\u003e\n\n# @colordx/core\n\n[![npm version](https://img.shields.io/npm/v/@colordx/core?labelColor=764be5\u0026color=ffc200)](https://www.npmjs.com/package/@colordx/core)\n[![used by cssnano](https://img.shields.io/badge/used_by-cssnano-ffc200?labelColor=764be5)](https://github.com/cssnano/cssnano)\n[![bundle size](https://img.shields.io/bundlejs/size/@colordx/core?labelColor=764be5\u0026color=ffc200)](https://bundlejs.com/?q=@colordx/core)\n[![npm downloads](https://img.shields.io/npm/dw/@colordx/core?labelColor=764be5\u0026color=ffc200)](https://www.npmjs.com/package/@colordx/core)\n[![zero dependencies](https://img.shields.io/badge/dependencies-0-ffc200?labelColor=764be5)](https://github.com/dkryaklin/colordx/blob/main/package.json)\n[![CI](https://img.shields.io/github/actions/workflow/status/dkryaklin/colordx/ci.yml?branch=main\u0026label=ci\u0026labelColor=764be5\u0026color=ffc200)](https://github.com/dkryaklin/colordx/actions/workflows/ci.yml)\n\n**[Try it on colordx.dev](https://colordx.dev)**\n\nA modern color manipulation library built for the CSS Color 4 era. The successor to [colord](https://github.com/omgovich/colord) with first-class support for **OKLCH** and **OKLab**. **5 KB gzipped. More than 2× faster than colord.**\n\n## Why colordx?\n\n[colord](https://github.com/omgovich/colord) is a great library, but it was designed around CSS Color 3. Modern CSS uses `oklch()` and `oklab()` — color spaces that produce better gradients, more accurate lightness adjustments, and consistent hue shifts. colord has no support for them, not even via a plugin. With colordx, they're built in.\n\n## Performance\n\nBenchmarks run on Apple M4, Node.js 22, using [mitata](https://github.com/evanwashere/mitata). Operations per second — higher is better.\n\n| Benchmark | **colordx** | @texel/color | colord | culori | chroma-js | color | tinycolor2 |\n|---|---|---|---|---|---|---|---|\n| HEX → toHsl | **13M** | — | 5.2M | 3.4M | 2.4M | 1.7M | 1.7M |\n| HEX → lighten → toHex | **9.9M** | — | 3.8M | 3.1M | 780K | 650K | 650K |\n| Mix two colors | **6.3M** | 4.8M | 1.4M | 920K | 1.0M | 460K | 1.1M |\n| HEX → toOklch | **5.6M** | 4.2M | — | 3.3M | 1.0M | 1.8M | — |\n| inGamutP3 | **4.4M** | 3.0M | — | 960K | — | — | — |\n| inGamutRec2020 | **4.1M** | 3.0M | — | 1.0M | — | — | — |\n\n## Install\n\n```bash\nnpm install @colordx/core\n```\n\n## Quick start\n\n```ts\nimport { colordx } from '@colordx/core';\n\n// Parse any CSS color string or color object, then chain conversions:\ncolordx('#ff0000').toRgbString();     // 'rgb(255 0 0)'\ncolordx('#ff0000').toHex();           // '#ff0000'\ncolordx('#ff0000').toOklch();         // { l: 0.628, c: 0.2577, h: 29.23, alpha: 1 }\ncolordx('#ff0000').toOklchString();   // 'oklch(0.628 0.2577 29.23)'\n\n// Works from any input format — hex, rgb(), hsl(), oklch(), oklab(), plain objects:\ncolordx('oklch(0.5 0.2 240)').toHex();                     // '#0069c7'\ncolordx({ r: 255, g: 0, b: 0, alpha: 1 }).toHslString();   // 'hsl(0 100% 50%)'\n\n// Chain manipulations — each call returns a new immutable Colordx:\ncolordx('#ff0000').lighten(0.1).saturate(0.2).toHex();\ncolordx('#3d7a9f').rotate(30).darken(0.1).toRgbString();\n```\n\nThe `colordx()` factory is all you need for day-to-day work. For out-of-gamut `oklch()` / `oklab()` inputs, `.toHex()` / `.toRgbString()` clip in linear sRGB — the same strategy browsers use when rendering `background: oklch(...)` — so your output matches what users see on screen. If you need stricter hue/lightness preservation for authoring workflows, see [Gamut](#gamut).\n\n## API\n\nAll methods are immutable — they return a new `Colordx` instance.\n\n### Parsing\n\nAccepts any CSS color string or color object:\n\n```ts\ncolordx('#ff0000');\ncolordx('#f00');\ncolordx('rgb(255 0 0)');\ncolordx('rgba(255, 0, 0, 0.5)');\ncolordx('hsl(0 100% 50%)');\ncolordx('oklab(0.6279 0.2249 0.1257)');\ncolordx('oklch(0.6279 0.2577 29.23)');\ncolordx({ r: 255, g: 0, b: 0, alpha: 1 });\ncolordx({ h: 0, s: 100, l: 50, alpha: 1 });\ncolordx({ l: 0.6279, a: 0.2249, b: 0.1257, alpha: 1 }); // OKLab\ncolordx({ l: 0.6279, c: 0.2577, h: 29.23, alpha: 1 }); // OKLch\n// With p3 plugin loaded:\ncolordx('color(display-p3 0.9176 0.2003 0.1386)'); // Display-P3 string\n// With rec2020 plugin loaded:\ncolordx('color(rec2020 0.7919 0.2307 0.0739)'); // Rec.2020 string\n// With hwb plugin loaded:\ncolordx('hwb(0 0% 0%)');\ncolordx({ h: 0, w: 0, b: 0, alpha: 1 });\n// With hsv plugin loaded:\ncolordx({ h: 0, s: 100, v: 100, alpha: 1 }); // HSV\n```\n\n### Conversion\n\n```ts\n.toRgb()           // { r: 255, g: 0, b: 0, alpha: 1 }\n.toRgbString()     // 'rgb(255 0 0)'\n.toHex()           // '#ff0000'\n.toNumber()        // 16711680  (0xff0000 — PixiJS / Discord integer format)\n.toHsl()           // { h: 0, s: 100, l: 50, alpha: 1 }\n.toHslString()     // 'hsl(0 100% 50%)'\n// toHsl accepts an optional precision argument (decimal places):\ncolordx('#3d7a9f').toHsl()         // { h: 202.65, s: 44.55, l: 43.14, alpha: 1 }      — default (2)\ncolordx('#3d7a9f').toHsl(4)        // { h: 202.6531, s: 44.5455, l: 43.1373, alpha: 1 }\ncolordx('#3d7a9f').toHsl(0)        // { h: 203, s: 45, l: 43, alpha: 1 }               — integers\ncolordx('#3d7a9f').toHslString()   // 'hsl(202.65 44.55% 43.14%)'\ncolordx('#3d7a9f').toHslString(4)  // 'hsl(202.6531 44.5455% 43.1373%)'\n// With hwb plugin loaded:\n.toHwb()           // { h: 0, w: 0, b: 0, alpha: 1 }\n.toHwbString()     // 'hwb(0 0% 0%)'\n.toOklab()         // { l: 0.628, a: 0.2249, b: 0.1258, alpha: 1 }\n.toOklabString()   // 'oklab(0.628 0.2249 0.1258)'\n.toOklch()         // { l: 0.628, c: 0.2577, h: 29.23, alpha: 1 }\n.toOklchString()   // 'oklch(0.628 0.2577 29.23)'\n// With p3 plugin loaded:\n.toP3()            // { r: 0.9175, g: 0.2003, b: 0.1386, alpha: 1, colorSpace: 'display-p3' }\n.toP3String()      // 'color(display-p3 0.9175 0.2003 0.1386)'\n```\n\n### Manipulation\n\n```ts\n.lighten(0.1)                        // increase lightness by 10 percentage points\n.lighten(0.1, { relative: true })    // increase lightness by 10% of current value\n.darken(0.1)                         // decrease lightness by 10 percentage points\n.darken(0.1, { relative: true })     // decrease lightness by 10% of current value\n.saturate(0.1)                       // increase saturation by 10 percentage points\n.saturate(0.1, { relative: true })   // increase saturation by 10% of current value\n.desaturate(0.1)                     // decrease saturation by 10 percentage points\n.desaturate(0.1, { relative: true }) // decrease saturation by 10% of current value\n.grayscale()       // fully desaturate\n.invert()          // invert RGB channels\n.rotate(30)        // rotate hue by 30°\n.alpha(0.5)        // set alpha\n.hue(120)          // set hue (HSL)\n.lightness(0.5)    // set lightness (OKLCH, 0–1)\n.chroma(0.1)       // set chroma (OKLCH, 0–0.4)\n```\n\n### Getters\n\n```ts\n.isValid()         // true if input was parseable\n.alpha()           // get alpha (0–1)\n.hue()             // get hue (0–360)\n.lightness()       // get OKLCH lightness (0–1)\n.chroma()          // get OKLCH chroma (0–0.4)\n.brightness()      // perceived brightness (0–1)\n.isDark()          // brightness \u003c 0.5\n.isLight()         // brightness \u003e= 0.5\n.isEqual('#f00')   // exact RGB equality\n// With a11y plugin loaded:\n.luminance()       // relative luminance (0–1, WCAG)\n.contrast('#fff')  // WCAG 2.x contrast ratio (1–21)\n// With mix plugin loaded:\n.mix('#0000ff', 0.5)       // mix in sRGB space (CSS spec)\n.mixOklab('#0000ff', 0.5)  // mix in Oklab space (perceptually uniform)\n```\n\n### Utilities\n\n```ts\nimport { getFormat, nearest, oklchToLinear, oklchToRgbChannels, random } from '@colordx/core';\n\ngetFormat('#ff0000'); // 'hex'\ngetFormat('rgb(255 0 0)'); // 'rgb'\ngetFormat('hsl(0 100% 50%)'); // 'hsl'\ngetFormat('oklch(0.5 0.2 240)'); // 'oklch'\ngetFormat('oklab(0.6279 0.2249 0.1257)'); // 'oklab'\ngetFormat({ r: 255, g: 0, b: 0, alpha: 1 }); // 'rgb'\ngetFormat({ h: 0, s: 100, l: 50, alpha: 1 }); // 'hsl'\ngetFormat('notacolor'); // undefined\n// Plugin-added parsers register their own format:\n// p3 → 'p3', hsv → 'hsv', cmyk → 'cmyk', lch → 'lch', lab → 'lab', xyz → 'xyz', names → 'name', rec2020 → 'rec2020'\n\nnearest('#800', ['#f00', '#ff0', '#00f']); // '#f00' — perceptual distance via OKLab\nnearest('#ffe', ['#f00', '#ff0', '#00f']); // '#ff0'\n\nrandom(); // random Colordx instance\n\n// Low-level functional converters — no object allocation, for hot paths (canvas gradients, etc.)\noklchToRgbChannels(0.5, 0.2, 240); // [r, g, b] gamma-encoded sRGB in [0, 1]\n// Out-of-gamut channels may exceed [0, 1] — callers clamp before byte encoding\n\nconst linear = oklchToLinear(0.5, 0.2, 240); // unclamped linear sRGB — also a free sRGB gamut check\n\n// P3/Rec.2020 channel functions live in their plugins:\nimport { linearToP3Channels, oklchToP3Channels } from '@colordx/core/plugins/p3';\nimport { linearToRec2020Channels, oklchToRec2020Channels } from '@colordx/core/plugins/rec2020';\n\noklchToP3Channels(0.5, 0.2, 240);      // [r, g, b] gamma-encoded Display-P3 in [0, 1]\noklchToRec2020Channels(0.5, 0.2, 240); // [r, g, b] gamma-encoded Rec.2020 in [0, 1] (BT.2020 gamma)\n\n// Split-step API: compute the shared expensive OKLCH→linear sRGB step once,\n// then apply cheap per-space steps to avoid repeating 3× Math.cbrt + OKLab matrix.\nlinearToP3Channels(...linear);      // linear sRGB → gamma-encoded P3\nlinearToRec2020Channels(...linear); // linear sRGB → gamma-encoded Rec.2020 (BT.2020 gamma)\n```\n\n**Zero-allocation tight-loop variants (`*Into`).** Every channel function has an `*Into` sibling that writes into a caller-provided `Float64Array | number[]` instead of allocating a new tuple. For per-pixel work (canvas renderers, gradient grids, wide-gamut data viz), this eliminates ~10× the GC pressure and makes interactive redraws smoother. Output is bit-for-bit identical to the allocating version.\n\n```ts\nimport {\n  oklchToLinearInto,\n  oklchToRgbChannelsInto,\n  oklchToLinearAndSrgbInto,\n} from '@colordx/core';\nimport { linearToP3ChannelsInto, oklchToP3ChannelsInto } from '@colordx/core/plugins/p3';\nimport { linearToRec2020ChannelsInto, oklchToRec2020ChannelsInto } from '@colordx/core/plugins/rec2020';\n\n// Pixel-renderer pattern: allocate one buffer, reuse for every pixel.\nconst buf = new Float64Array(3);\nfor (let y = 0; y \u003c height; y++) {\n  for (let x = 0; x \u003c width; x++) {\n    const [l, c, h] = getOklch(x, y);\n    oklchToP3ChannelsInto(buf, l, c, h);\n    imageData[i++] = Math.floor(buf[0] * 255);\n    imageData[i++] = Math.floor(buf[1] * 255);\n    imageData[i++] = Math.floor(buf[2] * 255);\n    imageData[i++] = 255;\n  }\n}\n```\n\nFull `*Into` surface (15 functions, all tree-shakable — unused ones have zero bundle cost):\n\n```ts\n// from '@colordx/core'\noklchToLinearInto(out, l, c, h);           // → [lr, lg, lb] linear sRGB\noklchToRgbChannelsInto(out, l, c, h);      // → [r, g, b] gamma-encoded sRGB\noklchToLinearAndSrgbInto(linOut, srgbOut, l, c, h); // both at once (distinct buffers)\n\n// from '@colordx/core/plugins/p3'\nlinearToP3ChannelsInto(out, lr, lg, lb);\noklchToP3ChannelsInto(out, l, c, h);\n\n// from '@colordx/core/plugins/rec2020'\nlinearToRec2020ChannelsInto(out, lr, lg, lb);\noklchToRec2020ChannelsInto(out, l, c, h);\n\n// Lower-level matrix / color-space primitives also have *Into siblings:\n// linearSrgbToOklabInto, oklabToLinearInto (from '@colordx/core')\n// xyzD50ToLinearSrgbInto, srgbLinearToP3LinearInto, linearP3ToSrgbInto,\n// oklabToLinearP3Into, srgbLinearToRec2020LinearInto, linearRec2020ToSrgbInto,\n// oklabToLinearRec2020Into\n```\n\nGuidance:\n- Use `Float64Array(3)` for the buffer when you can — it's the convention and keeps the V8 call site monomorphic. `number[]` also works.\n- One buffer per loop is plenty; don't allocate per iteration.\n- `linOut` and `srgbOut` in `oklchToLinearAndSrgbInto` **must be distinct buffers** (the function writes to both).\n- If you're outside a hot loop, the regular allocating versions are more ergonomic — reach for `*Into` only when you've profiled and GC is the bottleneck.\n\n### Gamut\n\n`oklch()` and `oklab()` can describe colors outside the sRGB gamut. **For everyday conversion, `.toRgbString()` / `.toHex()` already do the right thing** — they naive-clip in linear sRGB to match browser rendering, so your output matches what `background: oklch(...)` displays on screen. You only need the methods below when that default isn't what you want.\n\nInternally, out-of-gamut `oklch()` / `oklab()` inputs are stored **unclamped**, so the authored color is preserved losslessly. That means `.toOklchString()` round-trips the original, and you can choose when (and how) to fold the color into sRGB:\n\n```ts\nconst input = 'oklch(0.5 0.4 180)';  // out of sRGB gamut\n\n// 1. Preserve — keep the authored oklch as-is, clip only at sRGB output time\ncolordx(input).toOklchString();          // 'oklch(0.5 0.4 180)'\ncolordx(input).toRgbString();            // 'rgb(0 152 108)' — naive clip, matches browser\n\n// 2. Map — CSS Color 4 gamut mapping (preserves lightness + hue, reduces chroma)\ncolordx(input).mapSrgb().toOklchString();   // 'oklch(0.5091 0.0938 177.85)'\ncolordx(input).mapSrgb().toRgbString();     // 'rgb(0 119 102)'\n\n// 3. Clamp — naive-clip into sRGB as a Colordx (matches browser, but hue drifts)\ncolordx(input).clampSrgb().toOklchString(); // 'oklch(0.6012 0.1276 164.3)'\ncolordx(input).clampSrgb().toRgbString();   // 'rgb(0 152 108)' — same bytes as (1)\n```\n\n- **`.mapSrgb()`** — CSS Color 4 chroma-reduction binary search. Preserves lightness and hue; sacrifices chroma. Use when hue stability matters — design tokens, palettes, programmatic harmonies, OKLCH pickers.\n- **`.clampSrgb()`** — naive clip in linear sRGB. Hue and lightness may drift. Use when you want a `Colordx` whose `.toOklchString()` describes what browsers actually render.\n\nA static form is also available for one-shot conversion without wrapping first — `Colordx.toGamutSrgb(input)` is equivalent to `colordx(input).mapSrgb()`.\n\ncolordx also includes standalone utilities for checking and mapping into wider gamuts (Display-P3 / Rec.2020, via plugins):\n\n```ts\nimport { Colordx, inGamutSrgb } from '@colordx/core';\nimport { inGamutP3 } from '@colordx/core/plugins/p3';\nimport { inGamutRec2020 } from '@colordx/core/plugins/rec2020';\nimport p3 from '@colordx/core/plugins/p3';\nimport rec2020 from '@colordx/core/plugins/rec2020';\nextend([p3, rec2020]);\n\n// Check: is this color displayable in sRGB?\ninGamutSrgb('#ff0000'); // true  — hex is always sRGB\ninGamutSrgb('oklch(0.5 0.1 30)'); // true  — clearly in sRGB\ninGamutSrgb('oklch(0.5 0.4 180)'); // false — too much cyan chroma\n\n// Map: reduce chroma until in-gamut (preserves lightness and hue)\nColordx.toGamutSrgb('oklch(0.5 0.4 180)'); // → Colordx at the sRGB boundary\nColordx.toGamutSrgb('#ff0000'); // → unchanged, already in sRGB\n\n// Display-P3 gamut (wider than sRGB) — available after extend([p3])\ninGamutP3('oklch(0.64 0.27 29)'); // true  — inside P3 but outside sRGB\ninGamutP3('oklch(0.5 0.4 180)'); // false — outside P3\nColordx.toGamutP3('oklch(0.5 0.4 180)'); // → Colordx at the P3 boundary\n\n// Rec.2020 gamut (wider than P3) — available after extend([rec2020])\ninGamutRec2020('oklch(0.5 0.4 180)'); // false — outside Rec.2020\nColordx.toGamutRec2020('oklch(0.5 0.4 180)'); // → Colordx at the Rec.2020 boundary\n```\n\nGamut containment is hierarchical: sRGB ⊂ Display-P3 ⊂ Rec.2020. All `inGamut*` functions always return `true` for sRGB-bounded inputs (hex, rgb, hsl, hsv, hwb). The `toGamut*` functions use a binary chroma-reduction search following the [CSS Color 4 gamut mapping algorithm](https://www.w3.org/TR/css-color-4/#css-gamut-mapping).\n\n## Plugins\n\nOpt-in plugins for less common color spaces and utilities:\n\n```ts\nimport { extend } from '@colordx/core';\nimport a11y from '@colordx/core/plugins/a11y';\n// isReadable(), readableScore(), minReadable(), apcaContrast(), isReadableApca()\nimport cmyk from '@colordx/core/plugins/cmyk';\n// toCmyk(), toCmykString(), parses device-cmyk() strings and CMYK objects\nimport harmonies from '@colordx/core/plugins/harmonies';\n// harmonies()\nimport hwb from '@colordx/core/plugins/hwb';\n// toHwb(), toHwbString(), parses hwb() strings and HWB objects\nimport hsv from '@colordx/core/plugins/hsv';\n// toHsv(), toHsvString(), parses hsv() strings and HSV objects\nimport lab from '@colordx/core/plugins/lab';\n// toLab(), toLabString(), toXyz(), toXyzString(), mixLab(), delta(), parses Lab/XYZ objects\nimport lch from '@colordx/core/plugins/lch';\n// toLch(), toLchString(), parses lch() strings and LCH objects\nimport minify from '@colordx/core/plugins/minify';\n// minify() — shortest CSS string\nimport mix from '@colordx/core/plugins/mix';\n// tints(), shades(), tones(), palette()\nimport names from '@colordx/core/plugins/names';\n// toName(), parses CSS color names\nimport p3 from '@colordx/core/plugins/p3';\n// toP3(), toP3String(), inGamutP3(), Colordx.toGamutP3(), linearToP3Channels(), oklchToP3Channels(), parses color(display-p3 ...) strings\nimport rec2020 from '@colordx/core/plugins/rec2020';\n// toRec2020(), toRec2020String(), inGamutRec2020(), Colordx.toGamutRec2020(), linearToRec2020Channels(), oklchToRec2020Channels(), parses color(rec2020 ...) strings\n\nextend([lab, lch, cmyk, names, a11y, harmonies, hwb, hsv, mix, minify, p3, rec2020]);\n```\n\n### lab plugin\n\nCIE Lab (D50) and CIE XYZ (D50) color models. Lab and XYZ objects are also accepted as color input (Lab requires a `colorSpace: 'lab'` discriminant). Also adds `.mixLab()` for colord-compatible perceptual mixing, `.delta()` for CIEDE2000 color difference, and string conversion methods.\n\n```ts\nimport lab from '@colordx/core/plugins/lab';\n\nextend([lab]);\n\ncolordx('#ff0000').toLab(); // { l: 54.29, a: 80.8, b: 69.89, alpha: 1, colorSpace: 'lab' }\ncolordx('#ff0000').toLabString(); // 'lab(54.29 80.8 69.89)'\ncolordx('lab(54.29 80.8 69.89)').toHex(); // '#ff0000'  — lab strings are parseable\ncolordx('#ff0000').toXyz(); // { x: 43.61, y: 22.25, z: 1.39, alpha: 1 }\ncolordx('#ff0000').toXyzString(); // 'color(xyz-d65 43.61 22.25 1.39)'\n\n// Lab and XYZ objects parse as color input (with lab plugin loaded)\n// Lab objects require colorSpace: 'lab' to distinguish from OKLab (which has the same l/a/b shape)\ncolordx({ l: 54.29, a: 80.8, b: 69.89, alpha: 1, colorSpace: 'lab' as const }).toHex(); // '#ff0000'\ncolordx({ x: 43.61, y: 22.25, z: 1.39, alpha: 1 }).toHex(); // '#ff0000'\n\n// Mix in CIE Lab space (colord-compatible)\ncolordx('#000000').mixLab('#ffffff').toHex(); // '#777777'\n\n// CIEDE2000 perceptual color difference (0 = identical, ~1 = maximum)\ncolordx('#ff0000').delta('#ff0000'); // 0\ncolordx('#000000').delta('#ffffff'); // ~1\ncolordx('#ff0000').delta(); // compared against white (default)\n```\n\n### lch plugin\n\nCIE LCH (D50) — the polar form of CIE Lab. Parses `lch()` CSS strings and LCH objects.\n\n```ts\nimport lch from '@colordx/core/plugins/lch';\n\nextend([lch]);\n\ncolordx('#ff0000').toLch(); // { l: 54.29, c: 106.84, h: 40.86, alpha: 1, colorSpace: 'lch' }\ncolordx('#ff0000').toLchString(); // 'lch(54.29 106.84 40.86)'\ncolordx('lch(54.29 106.84 40.86)').toHex(); // '#ff0000'\n// LCH objects require colorSpace: 'lch' to distinguish from OKLCH (which has the same l/c/h shape)\ncolordx({ l: 50, c: 50, h: 180, alpha: 1, colorSpace: 'lch' as const }).toHex(); // parses as LCH object\n```\n\n### cmyk plugin\n\nCMYK color model. Parses `device-cmyk()` CSS strings and CMYK objects.\n\n```ts\nimport cmyk from '@colordx/core/plugins/cmyk';\n\nextend([cmyk]);\n\ncolordx('#ff0000').toCmyk(); // { c: 0, m: 100, y: 100, k: 0, alpha: 1 }\ncolordx('#ff0000').toCmykString(); // 'device-cmyk(0% 100% 100% 0%)'\ncolordx('device-cmyk(0% 100% 100% 0%)').toHex(); // '#ff0000'\ncolordx({ c: 0, m: 100, y: 100, k: 0, alpha: 1 }).toHex(); // '#ff0000'\n```\n\n### names plugin\n\nCSS named color support (140 names from the CSS spec). `toName()` returns `undefined` for colors with no CSS name.\n\n```ts\nimport names from '@colordx/core/plugins/names';\n\nextend([names]);\n\ncolordx('red').toHex(); // '#ff0000'\ncolordx('rebeccapurple').toHex(); // '#663399'\ncolordx('#ff0000').toName(); // 'red'\ncolordx('#c06060').toName(); // undefined — no CSS name for this color\ncolordx('#c06060').toName({ closest: true }); // nearest named color by RGB distance\n```\n\n### hsv plugin\n\nHSV/HSVa color model. Parses `hsv()` / `hsva()` strings and HSV objects.\n\n```ts\nimport hsv from '@colordx/core/plugins/hsv';\n\nextend([hsv]);\n\ncolordx('#ff0000').toHsv(); // { h: 0, s: 100, v: 100, alpha: 1 }\ncolordx('#ff0000').toHsvString(); // 'hsv(0 100% 100%)'\ncolordx('hsv(0 100% 100%)').toHex(); // '#ff0000'\ncolordx({ h: 0, s: 100, v: 100, alpha: 1 }).toHex(); // '#ff0000'\n```\n\n### harmonies plugin\n\nColor harmony generation using hue rotation.\n\n```ts\nimport harmonies from '@colordx/core/plugins/harmonies';\n\nextend([harmonies]);\n\ncolordx('#ff0000').harmonies();                              // complementary (default) — 2 colors\ncolordx('#ff0000').harmonies('complementary');               // [0°, 180°] — 2 colors\ncolordx('#ff0000').harmonies('analogous');                   // [−30°, 0°, 30°] — 3 colors\ncolordx('#ff0000').harmonies('split-complementary');         // [0°, 150°, 210°] — 3 colors\ncolordx('#ff0000').harmonies('triadic');                     // [0°, 120°, 240°] — 3 colors\ncolordx('#ff0000').harmonies('tetradic');                    // [0°, 90°, 180°, 270°] — 4 colors (square)\ncolordx('#ff0000').harmonies('rectangle');                   // [0°, 60°, 180°, 240°] — 4 colors\ncolordx('#ff0000').harmonies('double-split-complementary');  // [−30°, 0°, 30°, 150°, 210°] — 5 colors\n```\n\n### hwb plugin\n\nCSS Color Level 4 HWB (Hue, Whiteness, Blackness) color model.\n\n```ts\nimport hwb from '@colordx/core/plugins/hwb';\n\nextend([hwb]);\n\ncolordx('#ff0000').toHwb();         // { h: 0, w: 0, b: 0, alpha: 1 }\ncolordx('#ff0000').toHwbString();   // 'hwb(0 0% 0%)'\ncolordx('hwb(0 0% 0%)').toHex();   // '#ff0000'\ncolordx({ h: 0, w: 0, b: 0, alpha: 1 }).toHex(); // '#ff0000'\n\n// toHwb accepts an optional precision argument (decimal places):\ncolordx('#3d7a9f').toHwb();    // { h: 203, w: 24, b: 38, alpha: 1 }   — default (0)\ncolordx('#3d7a9f').toHwb(2);   // { h: 202.65, w: 23.92, b: 37.65, alpha: 1 }\ncolordx('#3d7a9f').toHwbString();  // 'hwb(203 24% 38%)'\ncolordx('#3d7a9f').toHwbString(2); // 'hwb(202.65 23.92% 37.65%)'\n```\n\n### mix plugin\n\nColor mixing helpers built on top of `.mix()`.\n\n```ts\nimport mix from '@colordx/core/plugins/mix';\n\nextend([mix]);\n\ncolordx('#ff0000').tints(5); // [#ff0000, #ff4040, #ff8080, #ffbfbf, #ffffff]\ncolordx('#ff0000').shades(3); // [#ff0000, #800000, #000000]\ncolordx('#ff0000').tones(3);  // [#ff0000, #c04040, #808080]\n\n// palette: N evenly-spaced stops toward any target (default: white)\ncolordx('#ff0000').palette(3, '#0000ff'); // [#ff0000, #800080, #0000ff]\n```\n\n### minify plugin\n\nReturns the shortest valid CSS representation of a color. By default tries hex, RGB, and HSL and picks the shortest.\n\n```ts\nimport minify from '@colordx/core/plugins/minify';\n\nextend([minify]);\n\ncolordx('#ff0000').minify(); // '#f00'\ncolordx('#ffffff').minify(); // '#fff'\ncolordx('#ff0000').minify({ name: true }); // 'red'  — requires names plugin\ncolordx({ r: 0, g: 0, b: 0, a: 0 }).minify({ transparent: true }); // 'transparent'\ncolordx({ r: 255, g: 0, b: 0, a: 0.5 }).minify({ alphaHex: true }); // '#ff000080'\n\n// Disable specific formats to exclude them from candidates:\ncolordx('#ff0000').minify({ hsl: false }); // skips HSL, picks from hex/RGB\n```\n\n### a11y plugin\n\nWCAG 2.x contrast:\n\n```ts\ncolordx('#000').isReadable('#fff'); // true  — AA normal (ratio \u003e= 4.5)\ncolordx('#000').isReadable('#fff', { level: 'AAA' }); // true  — AAA normal (ratio \u003e= 7)\ncolordx('#000').isReadable('#fff', { size: 'large' }); // true  — AA large (ratio \u003e= 3)\ncolordx('#000').readableScore('#fff'); // 'AAA'\ncolordx('#e60000').readableScore('#ffff47'); // 'AA'\ncolordx('#949494').readableScore('#fff'); // 'AA large'\ncolordx('#aaa').readableScore('#fff'); // 'fail'\ncolordx('#777').minReadable('#fff'); // darkened/lightened to reach 4.5\n```\n\nAPCA (Accessible Perceptual Contrast Algorithm) — the projected replacement for WCAG 2.x in WCAG 3.0:\n\n```ts\n// Returns a signed Lc value: positive = dark text on light bg, negative = light text on dark bg\ncolordx('#000').apcaContrast('#fff'); //  106.0\ncolordx('#fff').apcaContrast('#000'); // -107.9\ncolordx('#202122').apcaContrast('#cf674a'); //  37.2  ← dark text on orange\ncolordx('#ffffff').apcaContrast('#cf674a'); // -69.5  ← white text on orange\n\n// Checks readability using |Lc| thresholds: \u003e= 75 for normal text, \u003e= 60 for large text/headings\ncolordx('#000').isReadableApca('#fff'); // true\ncolordx('#777').isReadableApca('#fff'); // false\ncolordx('#777').isReadableApca('#fff', { size: 'large' }); // true\n```\n\nAPCA is better suited than WCAG 2.x for dark color pairs and more accurately reflects human perception. See [Introduction to APCA](https://git.apcacontrast.com/documentation/APCAeasyIntro) for background.\n\n### p3 plugin\n\nAdds Display-P3 color space support. P3 has a wider gamut than sRGB and is natively supported by all modern browsers and most Mac/iOS displays.\n\n```ts\nimport p3 from '@colordx/core/plugins/p3';\n\nextend([p3]);\n\ncolordx('#ff0000').toP3(); // { r: 0.9175, g: 0.2003, b: 0.1386, alpha: 1, colorSpace: 'display-p3' }\ncolordx('#ff0000').toP3String(); // 'color(display-p3 0.9175 0.2003 0.1386)'\n\n// Parse Display-P3 strings (alpha optional)\ncolordx('color(display-p3 0.9175 0.2003 0.1386)').toHex(); // '#ff0000'\ncolordx('color(display-p3 0.9175 0.2003 0.1386 / 0.5)').toHex(); // '#ff000080'\n```\n\nThe plugin also exports standalone gamut utilities and low-level channel functions. `inGamutP3` and the channel helpers need no `extend()`. Gamut mapping is available as `Colordx.toGamutP3` after `extend([p3])`:\n\n```ts\nimport { Colordx, extend } from '@colordx/core';\nimport p3, { inGamutP3, linearToP3Channels, oklchToP3Channels } from '@colordx/core/plugins/p3';\n\nextend([p3]);\n\ninGamutP3('oklch(0.64 0.27 29)');        // true — inside P3 but outside sRGB\nColordx.toGamutP3('oklch(0.5 0.4 180)'); // → Colordx at the P3 boundary\n\noklchToP3Channels(0.5, 0.2, 240); // [r, g, b] gamma-encoded P3 in [0, 1]\n```\n\nObject parsing is also supported using the `colorSpace` discriminant:\n\n```ts\ncolordx({ r: 0.9505, g: 0.2856, b: 0.0459, alpha: 1, colorSpace: 'display-p3' }).toHex();\n```\n\n### rec2020 plugin\n\nAdds Rec.2020 (BT.2020) color space support. Rec.2020 has the widest gamut of the three — it covers most of the visible spectrum.\n\n```ts\nimport rec2020 from '@colordx/core/plugins/rec2020';\n\nextend([rec2020]);\n\ncolordx('#ff0000').toRec2020(); // { r: 0.792, g: 0.231, b: 0.0738, alpha: 1, colorSpace: 'rec2020' }\ncolordx('#ff0000').toRec2020String(); // 'color(rec2020 0.792 0.231 0.0738)'\n\n// Parse Rec.2020 strings (alpha optional)\ncolordx('color(rec2020 0.792 0.231 0.0738)').toHex(); // '#ff0000'\ncolordx('color(rec2020 0.792 0.231 0.0738 / 0.5)').toHex(); // '#ff000080'\n```\n\nThe plugin also exports standalone gamut utilities and low-level channel functions. `inGamutRec2020` and the channel helpers need no `extend()`. Gamut mapping is available as `Colordx.toGamutRec2020` after `extend([rec2020])`:\n\n```ts\nimport { Colordx, extend } from '@colordx/core';\nimport rec2020, { inGamutRec2020, linearToRec2020Channels, oklchToRec2020Channels } from '@colordx/core/plugins/rec2020';\n\nextend([rec2020]);\n\ninGamutRec2020('oklch(0.5 0.4 180)');        // false — outside Rec.2020\nColordx.toGamutRec2020('oklch(0.5 0.4 180)'); // → Colordx at the Rec.2020 boundary\n\noklchToRec2020Channels(0.5, 0.2, 240); // [r, g, b] gamma-encoded Rec.2020 in [0, 1]\n```\n\nObject parsing is also supported using the `colorSpace` discriminant:\n\n```ts\ncolordx({ r: 0.7919, g: 0.2307, b: 0.0739, alpha: 1, colorSpace: 'rec2020' }).toHex();\n```\n\n## Migrating from colord\n\nThe API is intentionally compatible. Most code works unchanged:\n\n```ts\n// Before\nimport { colord } from 'colord';\nconst c = colord('#ff0000');\n\n// After\nimport { colordx } from '@colordx/core';\nconst c = colordx('#ff0000');\n```\n\n### What's the same\n\nAll core manipulation and conversion methods have identical signatures:\n`.toHex()`, `.toRgb()`, `.toRgbString()`, `.toHsl()`, `.toHslString()`, `.lighten()`, `.darken()`, `.saturate()`, `.desaturate()`, `.grayscale()`, `.invert()`, `.rotate()`, `.alpha()`, `.hue()`, `.brightness()`, `.isDark()`, `.isLight()`, `.isEqual()`, `getFormat()`, `random()`\n\nThe following remain **plugin-only** (same as colord): `.mix()`, `.mixOklab()`, `.luminance()`, `.contrast()`, `.toHwb()`, `.toHwbString()`.\n\n`.lighten()`, `.darken()`, `.saturate()`, and `.desaturate()` accept an optional `{ relative: true }` flag not present in colord — see [Relative lighten/darken](#relative-lightendarken) below.\n\n### What changed\n\n**HSV moved to a plugin:**\n\n```ts\n// colord\ncolord('#ff0000').toHsv();\n\n// colordx\nimport hsv from '@colordx/core/plugins/hsv';\nextend([hsv]);\ncolordx('#ff0000').toHsv();\n```\n\n**OKLCH and OKLab are now core** — no plugin needed:\n\n```ts\n// colord (requires plugin — not available)\n// colordx\ncolordx('#ff0000').toOklch();\ncolordx('#ff0000').toOklchString();\ncolordx('oklch(0.5 0.2 240)').toHex();\n```\n\n**CIE Lab, LCH, XYZ, CMYK moved to plugins:**\n\n```ts\n// colord\nimport { colord, extend } from 'colord';\nimport labPlugin from 'colord/plugins/lab';\nimport lchPlugin from 'colord/plugins/lch';\nimport xyzPlugin from 'colord/plugins/xyz';\nimport cmykPlugin from 'colord/plugins/cmyk';\nextend([labPlugin, lchPlugin, xyzPlugin, cmykPlugin]);\n\n// colordx\nimport { colordx, extend } from '@colordx/core';\nimport lab from '@colordx/core/plugins/lab';\nimport lch from '@colordx/core/plugins/lch';\nimport cmyk from '@colordx/core/plugins/cmyk';\n// Note: XYZ is part of the lab plugin in colordx\nextend([lab, lch, cmyk]);\n```\n\n**`getFormat()` import path:**\n\n```ts\n// colord\nimport { getFormat } from 'colord';\n\n// colordx\nimport { getFormat } from '@colordx/core';\n```\n\n**Alpha channel property renamed from `a` to `alpha`:**\n\ncolord used `a` as the alpha key in all color objects. colordx uses `alpha` everywhere (except OKLab and CIE Lab where `a` is a color axis).\n\n```ts\n// colord\ncolord('#ff0000').toRgb(); // { r: 255, g: 0, b: 0, a: 1 }\ncolord({ r: 255, g: 0, b: 0, a: 1 });\n\n// colordx\ncolordx('#ff0000').toRgb(); // { r: 255, g: 0, b: 0, alpha: 1 }\ncolordx({ r: 255, g: 0, b: 0, alpha: 1 });\n```\n\n### `mix()` uses sRGB; use `mixLab()` or `mixOklab()` for perceptual blending\n\ncolord's `mix` plugin interpolated in **CIE Lab** space. colordx's `mix()` uses **sRGB interpolation**, matching CSS `color-mix(in srgb, ...)` and how browsers composite layers.\n\n```ts\ncolordx('#000000').mix('#ffffff').toHex();       // '#808080' — sRGB (CSS spec)\ncolordx('#000000').mixOklab('#ffffff').toHex();  // '#636363' — Oklab (perceptually uniform)\n\n// colord-compatible Lab mixing — requires lab plugin\nimport lab from '@colordx/core/plugins/lab';\nextend([lab]);\ncolordx('#000000').mixLab('#ffffff').toHex();    // '#777777' — CIE Lab (colord-compatible)\n```\n\nThe same applies to `tints()`, `shades()`, and `tones()` from the mix plugin, which all call `.mix()` internally. If you have hardcoded expected hex values from colord's mix output, switch to `.mixLab()` or update the values.\n\n### `contrast()` rounding\n\ncolord uses `Math.floor` when rounding the WCAG contrast ratio to 2 decimal places; colordx uses standard rounding (`Math.round`). This affects values that fall exactly at .xxx5:\n\n```ts\ncolord('#ff0000').contrast('#ffffff'); // 3.99  (floor)\ncolordx('#ff0000').contrast('#ffffff'); // 4     (round)\n```\n\n### HSL precision\n\ncolordx returns higher precision HSL/HSV values than colord. If your code does exact equality checks on `.toHsl()` output, use `toBeCloseTo` or round the values.\n\n`toHsl()` now accepts an optional `precision` argument to control decimal places:\n\n```ts\ncolordx('#3d7a9f').toHsl(); // { h: 202.65, s: 44.55, l: 43.14, alpha: 1 }  — default (2)\ncolordx('#3d7a9f').toHsl(4); // { h: 202.6531, s: 44.5455, l: 43.1373, alpha: 1 }\ncolordx('#3d7a9f').toHsl(0); // { h: 203, s: 45, l: 43, alpha: 1 }\n```\n\nThe `minify()` plugin preserves full HSL precision when building candidates, so minification is now lossless — it only picks HSL when the string is genuinely shorter than hex/rgb.\n\n## Relative lighten/darken\n\nBy default, `.lighten(0.1)` shifts lightness by an **absolute** 10 percentage points (same as colord). Pass `{ relative: true }` to shift by a fraction of the **current** value instead — useful when migrating from Qix's `color` library or when you want proportional adjustments:\n\n```ts\n// Color with l=10%\ncolordx('#1a0000').lighten(0.1); // l = 10 + 10 = 20%  (absolute)\ncolordx('#1a0000').lighten(0.1, { relative: true }); // l = 10 * 1.1 = 11% (relative)\n\n// Color with s=40%\ncolordx('#a35050').saturate(0.1); // s = 40 + 10 = 50%  (absolute)\ncolordx('#a35050').saturate(0.1, { relative: true }); // s = 40 * 1.1 = 44% (relative)\n```\n\nThe same flag works on `.darken()` and `.desaturate()`.\n\n## Roadmap\n\n### CSS Color 4/5 completeness\n\n- **`color-mix()`** — parse and evaluate `color-mix(in oklch, red 30%, blue)` strings, with support for all interpolation spaces and polar hue methods (`shorter`, `longer`, `increasing`, `decreasing`)\n- **`color()` for remaining spaces** — `color(srgb ...)`, `color(srgb-linear ...)`, `color(a98-rgb ...)`, `color(prophoto-rgb ...)`, `color(xyz-d50 ...)`, `color(xyz-d65 ...)` string parsing (`display-p3` and `rec2020` already supported)\n- **Relative color syntax** — `oklch(from red l c h)` and channel arithmetic like `oklch(from red l calc(c + 0.1) h)`\n\n### Internals\n\n- Deduplicate the sRGB→XYZ D65 matrix shared between `xyz.ts` and `lab.ts`\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkryaklin%2Fcolordx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdkryaklin%2Fcolordx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkryaklin%2Fcolordx/lists"}