npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

precise-colors

v1.1.0

Published

Color conversions and Delta E functions without intermediate rounding

Downloads

672

Readme

npm version

precise-colors

High-precision color space conversions without intermediate rounding (except *2css, *2str, *2hex output functions).

Goals

  • Chain multiple conversions
    • Round only at the end using Math.round or provided roundTo, *2css, *2hex functions.
  • Make it clear in JSDoc the input/output ranges the user operates on and {@link Type} references for IDE navigation, preserved in .d.ts files.
  • Type-safe Lab illuminants: LabD65 (traditional) and LabD50 (CSS Color 4) prevent accidentally mixing values at compile time.

Install

npm install precise-colors

Usage

import { rgb2lab, lab2lch, lch2lab, lab2lyz, xyz2rgb, roundTo, rgb2css } from 'precise-colors'

const original = { r: 128, g: 64, b: 192 }

// RGB → Lab → LCH → Lab → XYZ → RGB round-trip
const lab = rgb2lab(original)           // { l: 41.31, a: 51.57, b: -56.63 }
const lch = lab2lch(lab)                // { l: 41.31, c: 76.59, h: 312.33 }
const backToLab = lch2lab(lch)
const xyz = lab2lyz(backToLab)
const rgb = xyz2rgb({ x: xyz.l, y: xyz.y, z: xyz.z })

// rgb = { r: 127.999997, g: 64.000014, b: 192.000002 }
// Error: ~0.00001 per channel

// Final step: round to integers or use output functions
Math.round(rgb.r)        // 128
roundTo(rgb.r, 2)        // 128.00 (avoids floating-point errors)
rgb2css(rgb)             // "rgb(128,64,192)"

Supported Color Spaces

| Space | Range | Description | | ------ | ---------------------------- | -------------------------------- | | RGB | r,g,b: 0-255 | sRGB (8-bit) | | HSL | h: 0-360, s,l: 0-100 | Hue, Saturation, Lightness | | HSV | h: 0-360, s,v: 0-100 | Hue, Saturation, Value | | HWB | h: 0-360, w,b: 0-100 | Hue, Whiteness, Blackness | | HCG | h: 0-360, c,g: 0-100 | Hue, Chroma, Grayness | | CMYK | c,m,y,k: 0-100 | Cyan, Magenta, Yellow, Key | | LabD65 | L: 0-100, a,b: ±128 | CIE L*a*b* (D65) | | LabD50 | L: 0-100, a,b: ±128 | CIE L*a*b* (D50, CSS Color 4) | | LCH | L: 0-100, C: 0-230, H: 0-360 | CIE LCH (cylindrical Lab) | | Oklab | L: 0-1, a,b: ±0.4 | Oklab (perceptually uniform) | | Oklch | L: 0-1, C: 0-0.4, H: 0-360 | Oklch (cylindrical Oklab) | | XYZ | x: 0-95, y: 0-100, z: 0-109 | CIE XYZ (D65) | | Apple | r16,g16,b16: 0-65535 | Apple 16-bit RGB | | Gray | 0-100 | Grayscale |

Conversion Matrix

⤴ = row to column ⤶ = column to row ° = via intermediate (see Chaining Conversions)

| | RGB | HSL | HSV | HWB | HCG | CMYK | LabD65 | LabD50 | LCH | Oklab | Oklch | XYZ | Apple | Gray | | ------ | --- | --- | --- | --- | --- | ---- | ------ | ------ | --- | ----- | ----- | --- | ----- | ---- | | RGB | ⟍ | ⤶⤴ | ⤶ | ⤶⤴ | ⤶ | ⤶⤴ | ⤶⤴ | ⤶⤴ | ° | ⤶⤴ | ⤶⤴ | ⤶⤴ | ⤶⤴ | ⤶ | | HSL | ⤴⤶ | ⟍ | ⤶⤴ | | ⤶⤴ | | | | | | | | | ⤶ | | HSV | ⤴ | ⤴⤶ | ⟍ | | ⤶⤴ | | | | | | | | | ⤶ | | HWB | ⤴⤶ | | | ⟍ | ⤶⤴ | | | | | | | | | ⤶ | | HCG | ⤴ | ⤴⤶ | ⤴⤶ | ⤴⤶ | ⟍ | | | | | | | | | | | CMYK | ⤴⤶ | | | | | ⟍ | | | | | | | | ⤶ | | LabD65 | ⤴⤶ | | | | | | ⟍ | ° | ⤶⤴ | | | ⤶⤴ | | ⤶ | | LabD50 | ⤴⤶ | | | | | | ° | ⟍ | | | | | | | | LCH | ° | | | | | | ⤴⤶ | | ⟍ | | | | | | | Oklab | ⤴⤶ | | | | | | | | | ⟍ | ⤶⤴ | | | | | Oklch | ⤴⤶ | | | | | | | | | ⤴⤶ | ⟍ | | | | | XYZ | ⤴⤶ | | | | | | ⤴⤶ | | | | | ⟍ | | | | Apple | ⤴⤶ | | | | | | | | | | | | ⟍ | | | Gray | ⤴ | ⤴ | ⤴ | ⤴ | | ⤴ | ⤴ | | | | | | | ⟍ |

Precision

Round-trip precision verified across all 16,777,216 RGB colors:

| Conversion | Max Error | | --------------- | --------- | | RGB → HSL → RGB | < 0.01 | | RGB → HSV → RGB | < 0.01 | | RGB → HWB → RGB | < 0.01 | | RGB → HCG → RGB | < 0.01 | | RGB → Lab → RGB | < 0.001 | | Lab → LCH → Lab | < 1e-6 |

Standards Compliance

  • CIE Lab: Exact rational constants per CIE 15.3 (ε = 216/24389, κ = 24389/27)
  • D65 white point: X=95.047, Y=100, Z=108.883
  • D50 white point: X=96.422, Y=100, Z=82.521 (CSS Color 4)
  • sRGB↔XYZ: IEC 61966-2-1 matrices (D65 native, D50 Bradford-adapted)
  • Oklab: Björn Ottosson matrices

CSS Color 4 Compatibility

Use rgb2labD50 / labD502rgb for Lab values matching CSS lab() function:

import { rgb2labD50, labD502rgb, LabD50 } from 'precise-colors'

// CSS Color 4 uses D50 white point
const lab: LabD50 = rgb2labD50({ r: 255, g: 0, b: 0 })
// → { l: 54.29, a: 80.81, b: 69.89 }

// Convert back
const rgb = labD502rgb({ l: 50, a: 0, b: 0 })
// → { r: 119, g: 119, b: 119 } (mid-gray)

Note: rgb2lab uses D65 (traditional), rgb2labD50 uses D50 (CSS Color 4).

Oklab / Oklch

Oklab is a perceptually uniform color space with better hue linearity than CIE Lab:

import { rgb2oklab, oklab2rgb, rgb2oklch, oklch2rgb, oklab2css, oklch2css } from 'precise-colors'

const oklab = rgb2oklab({ r: 255, g: 128, b: 64 })
// → { l: 0.7927, a: 0.0894, b: 0.1191 }

const oklch = rgb2oklch({ r: 255, g: 128, b: 64 })
// → { l: 0.7927, c: 0.1489, h: 53.12 }

// CSS Color 4 output
oklab2css(oklab)  // "oklab(0.7927 0.0894 0.1191)"
oklch2css(oklch)  // "oklch(0.7927 0.1489 53.12)"

CSS output functions: oklab2css, oklch2css, labD502css, lchD502css

LCH vs Oklch

Both are cylindrical (Lightness, Chroma, Hue) representations but differ in their base color space:

| | LCH (CIE LCHab) | Oklch | | -------------- | ------------------------------------------------ | ----------------------------- | | Base space | CIE Lab (1976) | Oklab (2020) | | L range | 0–100 | 0–1 | | CSS function | lch() | oklch() | | CSS illuminant | D50 | D65 | | Hue linearity | Hue shifts when adjusting L/C (especially blues) | Hue stays visually consistent |

Why Oklch? CIE Lab has a known issue: "inability to predict hue. In particular blue hues are predicted badly." Oklab was optimized for perceptual uniformity so L, C, and H can be adjusted independently without affecting perceived hue.

When to use LCH? Compatibility with existing Lab workflows, ICC profiles, or tools that expect CIE Lab values.

Color Difference (ΔE)

Functions to measure perceptual distance between colors:

| Function | Description | | ------------- | ---------------------------------------- | | deltaE76 | CIE76 - Euclidean in Lab (fast) | | deltaE94 | CIE94 - weighted for graphics/textiles | | deltaE2000 | CIEDE2000 - industry standard (accurate) | | deltaEOk | Euclidean in Oklab (modern, simple) |

import { rgb2lab, rgb2oklab, deltaE76, deltaE2000, deltaEOk } from 'precise-colors'

const red = rgb2lab({ r: 255, g: 0, b: 0 })
const orange = rgb2lab({ r: 255, g: 128, b: 0 })

deltaE76(red, orange)    // ~55.6 (simple Euclidean)
deltaE2000(red, orange)  // ~32.4 (perceptually weighted)

// Oklab alternative
const redOk = rgb2oklab({ r: 255, g: 0, b: 0 })
const orangeOk = rgb2oklab({ r: 255, g: 128, b: 0 })
deltaEOk(redOk, orangeOk)  // ~0.14

ΔE interpretation: 0 = identical, 1 ≈ just noticeable, 2-10 = perceptible at glance, 100 = opposite colors.

Type Safety

LabD65 and LabD50 are branded types that prevent mixing illuminants:

import { rgb2labD65, labD652rgb, rgb2labD50, labD502rgb, LabD65, LabD50 } from 'precise-colors'

const labD65: LabD65 = rgb2labD65({ r: 255, g: 0, b: 0 })
const labD50: LabD50 = rgb2labD50({ r: 255, g: 0, b: 0 })

labD652rgb(labD65) // ✓ Correct: D65 → D65 function
labD502rgb(labD50) // ✓ Correct: D50 → D50 function

// labD652rgb(labD50) // ✗ TypeScript error: LabD50 not assignable to LabD65
// labD502rgb(labD65) // ✗ TypeScript error: LabD65 not assignable to LabD50

Lab is an alias for LabD65. Similarly, rgb2lab/lab2rgb are aliases for rgb2labD65/labD652rgb.

Chaining Conversions

Conversions not directly supported can be achieved by chaining existing functions.

RGB ↔ LCH

import { rgb2lab, lab2lch, lch2lab, lab2rgb } from 'precise-colors'

// RGB → LCH
const lch = lab2lch(rgb2lab({ r: 128, g: 64, b: 192 }))
// → { l: 41.31, c: 76.59, h: 312.33 }

// LCH → RGB
const rgb = lab2rgb(lch2lab({ l: 50, c: 40, h: 270 }))
// → { r: 89.5, g: 111.8, b: 178.5 }

LabD65 ↔ LabD50

To convert between illuminants, go through RGB:

import { labD652rgb, rgb2labD50, labD502rgb, rgb2labD65, LabD65, LabD50 } from 'precise-colors'

// LabD65 → LabD50
function labD65toD50(lab: LabD65): LabD50 {
  return rgb2labD50(labD652rgb(lab))
}

// LabD50 → LabD65
function labD50toD65(lab: LabD50): LabD65 {
  return rgb2labD65(labD502rgb(lab))
}

// Example: same RGB color, different Lab representations
const labD65 = rgb2labD65({ r: 255, g: 128, b: 64 })
const labD50 = labD65toD50(labD65)
// labD65: { l: 67.33, a: 44.14, b: 55.35 }
// labD50: { l: 68.05, a: 46.41, b: 56.40 }

This approach guarantees round-trip consistency with sRGB and avoids additional error from separate chromatic adaptation.