culors
A Rust port of culori, the JavaScript color library by Dan Burzo. Covers color spaces, conversion, CSS Color Module 4 parsing and formatting, interpolation, gamut mapping, ΔE, blending, averaging, WCAG contrast, and CSS filters. Output values match culori 4.0.2 within 1e-10 across an exhaustive fixture set.
Used in production by Spectralite, a lighting-control application whose UI relies on culori. The Rust render core needed to produce the same colors as the JS layer, end to end. culors is the result.
Status
As of v1.5 culors tracks culori 4.0.2 at full feature parity, modulo the intentional divergences listed below. The library is in maintenance mode. New features land in culori first; once accepted upstream they are mirrored here. Bug fixes and culori version bumps remain in scope.
Features (v1.6)
| Feature | Coverage |
|---|---|
| Color spaces (33) | rgb, lrgb, hsl, hsv, hwb, lab (D50), lch (D50), lab65, lch65, oklab, oklch, xyz50, xyz65, p3, rec2020, a98, prophoto-rgb, cubehelix, dlab, dlch, jab, jch, yiq, hsi, hsluv, hpluv, okhsl, okhsv, itp, xyb, luv, lchuv, prismatic |
| Conversion | three flavors: direct From (typed, zero-overhead, bit-exact culori parity); generic convert<A, B> (typed, always XYZ-D65 hub, ~1e-14 drift); dynamic Color::convert_to(mode) and typed convert_culori<A, B> and reusable converter(mode) closure (culori's per-pair routing, which closes the 1e-14 gap with byte-for-byte parity) |
| CSS parser | named colors, hex, functional rgb/hsl/hwb/lab/lch/oklab/oklch, color() with srgb/srgb-linear/xyz/xyz-d50/xyz-d65/display-p3/rec2020/a98-rgb/prophoto-rgb/--lab-d65/--lch-d65, plus color-mix() |
| CSS formatters | format_css (canonical CSS Color Module 4 round-trip), plus legacy format_hex, format_hex8, format_rgb, format_hsl |
| Interpolation | interpolate / interpolate_with over all 33 spaces, hue-fixup (shorter, longer, increasing, decreasing, raw), per-channel easing, 7 spline interpolators (basis, basis-closed, natural, natural-closed, monotone, monotone-2, monotone-closed), interpolator_piecewise higher-order factory, and interpolate_with_premultiplied_alpha for clean transparent-to-color gradients |
| Easing | easing_midpoint, easing_smoothstep, easing_smoothstep_inverse, easing_smootherstep, easing_in_out_sine, easing_gamma |
| Gamut mapping | in_gamut, displayable, clamp_gamut, clamp_rgb, clamp_chroma, to_gamut (CSS Color Module 4 with ΔE OK). Accepts every culori-supported mode: sRGB family, wide-gamut p3/rec2020/a98/prophoto, and the unbounded spaces (lab, jab, itp, luv, …) which pass through unchanged, matching culori 4.0.2's getMode(mode).gamut table |
| ΔE | ciede76, ciede94, ciede2000, cmc, euclidean, hyab, hue_chroma, hue_saturation, hue_naive, ok, jz, itp, euclidean_xyz, kotsarenko_ramos |
| Blending | 16 modes: 12 separable (normal, multiply, screen, hard-light, overlay, darken, lighten, color-dodge, color-burn, soft-light, difference, exclusion) plus 4 non-separable from CSS Compositing 1 § 5.8 (hue, saturation, color, luminosity) |
| Averaging | average, average_number, average_angle (mode-aware, hue-circular). Same mode list as interpolate |
| Palette utilities | samples(n) / samples_with_easing(n, fn), nearest(palette, metric), round(places), random(mode) / random_with_constraints |
| Channel pipeline | mapper, map_alpha_multiply, map_alpha_divide, map_transfer_linear, map_transfer_gamma |
| Lerp utilities | lerp, unlerp, blerp, trilerp |
| WCAG | wcag_luminance, wcag_contrast |
| CSS filters | brightness, contrast, grayscale, hue-rotate, invert, saturate, sepia, plus CVD prot / deuter / trit |
| Fixture coverage | 110 conversion pairs, 365 parse cases, 303 format round-trips, plus per-pair convert_to parity fixtures, all verified against culori 4.0.2 |
| Tests | 1270 |
Installation
[]
= "1"
With serde support:
[]
= { = "1", = ["serde"] }
Quick start
use ;
let red = parse.unwrap;
let blue = parse.unwrap;
let mixed = blend;
let css = format_css;
assert!;
Convert in three flavors, each with different precision and ergonomic trade-offs:
use convert_culori;
use ;
use ;
let red = Rgb ;
// 1. Direct `From`. Typed, zero overhead, bit-for-bit culori parity on
// pairs where the impl exists. Best when both spaces are known at
// compile time.
let lab_direct: Lab = from;
// 2. Generic `convert<A, B>`. Typed, simple semantics, always routes
// through XYZ D65. ~1e-14 drift versus culori on pairs where culori
// takes a shorter path; back-compatible with v1.0 and v1.1 callers.
let lab_via_hub: Lab = convert;
// 3. `Color::convert_to` (dynamic) and `convert_culori<A, B>` (typed
// wrapper). Both match culori's `converter(mode)` dispatch exactly.
// Per-pair routing closes the 1e-14 gap.
let lab_culori: Lab = convert_culori;
let lab_dyn = Rgb.convert_to.unwrap;
// Cylindrical: oklch with hue fixup for grayscale.
let oklch: Oklch = from;
assert!;
Color::convert_to returns None when the target string is not a known mode; otherwise it produces the same routing culori would. Use it for CSS tooling, design-tool UIs, and any caller that carries the target space as a &str. Use convert_culori<A, B> when the source and target types are known at compile time but you still want culori's per-pair routing.
Interpolate between two colors in Oklab and sample at t = 0.5:
use ;
let a = parse.unwrap;
let b = parse.unwrap;
let ramp = interpolate;
let mid = ramp;
let _ = mid;
Comparison to culori
Every public function in culori 4.0.2 has a culors equivalent, with the exceptions listed under "Intentional divergences" below. The mapping is direct enough that culori code translates almost mechanically: culori.parse(s) becomes culors::parse(s), culori.convert(c, mode) becomes either the generic convert::<_, T>() or a direct T::from(c), and the curried difference, interpolate, and blend factories return Rust closures with the same shape.
| culori function | culors equivalent |
|---|---|
parse(str) |
parse(&str) |
formatCss(c) |
format_css(&c) |
converter(mode) |
Color::convert_to(mode), convert_culori::<_, T>(), convert::<_, T>(), or T::from(c) |
interpolate(colors, mode) |
interpolate(&colors, mode) |
inGamut(mode) / clampRgb / clampChroma / toGamut |
in_gamut, clamp_gamut, clamp_chroma, to_gamut |
differenceCiede76 … differenceItp |
difference_ciede76 … difference_itp |
blend |
blend, blend_str |
average |
average, average_number, average_angle |
wcagLuminance / wcagContrast |
wcag_luminance, wcag_contrast |
filterBrightness … filterDeficiencyTrit |
filter_brightness … filter_deficiency_trit |
colorsNamed table |
parse(name) (built-in) |
Intentional divergences from culori
- No runtime plugin registry. culori exposes
useMode,getMode,useParser, andremoveParserso JS callers can inject custom color spaces at runtime. culors is statically typed; new spaces are added at compile time. This is a language-design difference, not a missing feature. - Sub-parsers and sub-serializers are private. culori re-exports
parseHex,parseRgb,parseLab, …,serializeHex,serializeRgb, etc. for monkey-patching the parse and format chain. culors keeps these aspub(crate); the canonical entry points areparse()and theformat_*family. Behavior is fully equivalent. colorsNamedis internal. culori exposes the named-color map as a public dictionary. The culors equivalent is apub(crate)lookup table behindparse(name).- NaN channels render as
none. culori emits"NaN"for NaN channels incolor()formatting; culors emits"none"(CSS Color Module 4 spec-compliant). Cosmetic, applies only to artificially constructed inputs since culori's own pipeline never produces NaN. - Three culors-only color spaces.
Hsluv,Hpluv, andPrismaticare not in culori 4.0.2. Hsluv and Hpluv track the officialhsluv-javascriptreference; Prismatic follows Hauke 2009. - Three culors-only ΔE variants.
difference_ok,difference_jz, anddifference_euclidean_xyzare not exposed as named symbols by culori 4.0.2 (their math is reachable throughdifferenceEuclidean(mode, weights)instead). - Maintenance mode. As of v1.4, culors tracks culori 4.0.2 with the parity guarantees above. New features land in culori first; once accepted upstream they are mirrored here. Bug fixes and culori version bumps remain in scope.
Documentation
API reference on docs.rs. Release history is in CHANGELOG.md.
Contributing
Bug reports, fixture additions, and color-space implementations are welcome. The fixture generators under fixtures-gen/ consume culori 4.0.2 directly, so any drift between Rust and JavaScript output surfaces immediately when you regenerate. Run npm run gen-fixtures && npm run gen-parse-fixtures && npm run gen-format-fixtures before opening a PR; CI fails if regeneration produces a diff.
License
MIT.