8000 GitHub - rvanwijnen/spectral.js: Spectral.js is a paint like color mixing library utilizing the Kubelka-Munk theory.
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Spectral.js is a paint like color mixing library utilizing the Kubelka-Munk theory.

License

Notifications You must be signed in to change notification settings

rvanwijnen/spectral.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

68 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

logo Twitter URL

Spectral.js is a lightweight JavaScript library for realistic color mixing based on the Kubelka-Munk theory. It's designed specifically for developers and artists aiming to implement realistic pigment mixing in their projects.


🧭 Design Philosophy

Spectral.js is built on the idea that realistic color mixing should be both accurate and accessible. While most digital color tools rely on linear or additive RGB blending, Spectral.js takes a physically-informed approach by simulating how pigments interact with light using the Kubelka-Munk theory.

The goals behind Spectral.js:

  • 🎨 Physically-based: Pigment mixing is modeled using spectral data and light-matter interaction, not RGB math tricks.
  • βš–οΈ Perceptually aware: Uses color spaces like OKLab and OKLCh for perceptual uniformity and smart gamut mapping.
  • 🧠 Intelligent mixing: Every mix uses luminance, tinting strength, and user-defined intent to calculate effective concentration.
  • πŸš€ Optimized for performance: Lazy memoization and fast math make it lightweight and fast, even in browser environments.
  • πŸ› οΈ Simple API: Designed for developers and artists alike β€” works with familiar hex, RGB, and CSS strings.
  • πŸ“ Accurate under the hood: All calculations use 64-bit floating point math and are grounded in color science literature.

Whether you’re building a color picker, a digital painting app, or just experimenting with creative coding, Spectral.js gives you a closer approximation to how real pigments behave.

πŸ‘‰ You can try Spectral.js live at onedayofcrypto.art!


πŸ“– About Kubelka-M 8000 unk Theory

The Kubelka-Munk theory is a color science model that predicts how light interacts with pigmented materials by focusing on three main optical properties:

  • Absorption (K): How the material absorbs incident light.
  • Scattering (S): How the material scatters light within it.
  • Reflectance (R): The proportion of light diffusely reflected from the surface.

By modeling the interplay between absorption and scattering, the theory predicts the reflectance of pigmented materials across the visible spectrum (approximately 380 to 750 nm) β€” which in turn determines their perceived color.

Spectral.js uses the "simplified" or single-constant Kubelka-Munk theory, where the scattering coefficient (S) is assumed to be constant. This assumption allows the Kubelka-Munk function ( F(R) ) to directly relate reflectance to absorption:

$$F(R) = \frac{(1 - R)^2}{2R} = \frac{K}{S}$$

This relationship enables easy estimation of relative absorption from reflectance data, particularly useful in color matching and pigment formulation.


🌈 How Spectral.js Works

Spectral.js generates a spectral reflectance curve from a given RGB triplet by calculating the weights for combining the seven primary spectral reflectance curves: White, Cyan, Magenta, Yellow, Red, Green, and Blue. The resulting spectral reflectance curve is then used to calculate the Kubelka-Munk ( K/S ) values which are used in the mix function.

What sets Spectral.js apart is its spectral mixing model, which simulates how real pigments combine based on light absorption and scattering β€” rather than relying on RGB math. When colors are mixed, it calculates an effective concentration for each pigment using the formula:

$$C = f^2 \cdot T^2 \cdot L$$

Where:

  • ( L ): luminance
  • ( T ): tinting strength β€” applied exponentially to emphasize its effect on stronger or weaker pigments
  • ( f ): user-defined mixing factor

This formula integrates luminance ((L)), tinting strength ((T)), and the user-defined mixing factor ((f))β€”capturing how strongly a pigment influences the mix based on both its optical weight and intent. These concentrations are used to compute the weighted sum of each color’s Kubelka-Munk ( K/S ) value, resulting in a combined ( K/S_{mix} ) for the mixture.

Then the inverse Kubelka-Munk function is used which takes ( K/S_{mix} ) divided by the total concentration and returns the reflectance ( R ):

$$R = 1 + \left(\frac{K}{S}_{mix}\right) - \sqrt{\left(\frac{K}{S}_{mix}\right)^2 + 2 \cdot \left(\frac{K}{S}_{mix}\right)}$$

Using the CIE 1931 Color Matching Functions weighted with the D65 Standard Illuminant the reflectance ( R ) is then converted to CIE XYZ.

All spectral and colorimetric calculations are performed using 64-bit floating point precision ensuring stable results.


πŸ“¦ Installation

Include Spectral.js in your project easily via npm:

npm install spectral.js

Or download it from the releases.

Include the following script in your HTML:

<script src="spectral.js"></script>

πŸš€ Usage

Color

The heart of Spectral.js is its Color class, every function uses this as input and output.

let color = new spectral.Color('#002185');
let color = new spectral.Color('rgb(0, 33, 133)');
let color = new spectral.Color([0, 33, 133]);

Mix

The mix function takes any number of colors with factor and returns a new Color object. Whole numbers can be used for the factor, they will be normalized.

let color1 = new spectral.Color('#002185');
let color2 = new spectral.Color('#FCD200');

let mix = spectral.mix([color1, 0.5], [color2, 0.5]);

console.log(mix.toString()); //#3D933E

image1

let color1 = new spectral.Color('#FCF046');
let color2 = new spectral.Color('#E53166');
let color3 = new spectral.Color('#3375DA');

let mix = spectral.mix([color1, 1], [color2, 1], [color3, 1]);

console.log(mix.toString()); //#8D7964

image1

Palette

The palette function takes 2 colors and a size parameter, it returns an array of Color objects that gradually transition from color1 to color2 with the specified size.

let color1 = new spectral.Color('#BB0657');
let color2 = new spectral.Color('#E0E0B3');

let palette = spectral.palette(color1, color2, 8);

console.log(palette.map((x) => x.toString())); //['#BB0657', '#C00C5C', '#CD1E6F', '#DD3889', '#E85AA0', '#ED85AF', '#E9B9B4', '#E0E0B3']

image1

Gradient

The gradient function takes any number of colors with color-stop positions and returns a new Color object at a given position. Positions are between 0 and 1.

let color1 = new spectral.Color('#005E72');
let color2 = new spectral.Color('#EAD9A7');
let color3 = new spectral.Color('#894B54');

let t = 0.75; //get color at position t

let gradient = spectral.gradient(t, [color1, 0], [color2, 0.5], [color3, 1]);

console.log(gradient.toString()); //#C7938C

image1

Tinting Strength

Sometimes a Color is too dominant and this can be countered by adjusting the tinting strength. To my knowledge there is no way to programmatically determine if a Color is too dominant as this is pure perceptual.

let color1 = new spectral.Color('#ff0000');
let color2 = new spectral.Color('#ffff00');

let mix = spectral.mix([color1, 0.5], [color2, 0.5]);

console.log(mix.toString()); //#FF440F

image1

let color1 = new spectral.Color('#ff0000');
let color2 = new spectral.Color('#ffff00');

color1.tintingStrength = 0.35;

let mix = spectral.mix([color1, 0.5], [color2, 0.5]);

console.log(mix.toString()); //#FF8427

image1


🧡 GLSL Integration

The spectral.glsl file brings Spectral.js to GLSL for use in shaders. It enables real-time spectral color mixing directly on the GPU, perfect for generative art and WebGL projects.

  • Supports mixing between 2 to 4 colors.
  • Includes optional tinting strength control per color.
  • πŸ‘‰ A live demo is available on ShaderToy.

πŸ”§ Basic Usage

vec3 yellow = vec3(0.9734452903984108, 0.871367119198797000, 0.06124605423161808);
vec3 red    = vec3(0.7835377915261926, 0.030713443732993822, 0.13286832155381810);
vec3 blue   = vec3(0.0331047665708844, 0.177888415983629100, 0.70110189193297420);

vec3 col = spectral_mix(
	yellow, 1., 1. - p.x,
	red, 0.5, p.x - p.y,
	blue, 1., p.y
);

image1


πŸ› οΈ API Documentation

πŸ”Ή Color Class

The Color class internally computes spectral reflectance, XYZ, OKLab, OKLCh, and Kubelka-Munk parameters. To optimize performance, the class uses lazy memoization: values such as luminance, KS, and transforms to color spaces like OKLab and OKLCh are computed only once when accessed, then cached for future use, ensuring high performance without redundant calculations.

Methods and Properties:

  • color.R – Reflectance curve from 380 to 750 nm in 10 nm steps.
  • color.sRGB – sRGB color space representation.
  • color.lRGB – linear RGB color space representation.
  • color.XYZ – CIE XYZ color space representation.
  • color.OKLab – OKLab color space representation.
  • color.OKLCh – OKLCh color representation.
  • color.KS – Kubelka-Munk absorption/scattering parameters.
  • color.luminance – Luminance (Y value from CIE XYZ).
  • color.tintingStrength – Intensity of the pigment mixture (default: 1).
  • color.inGamut({ epsilon }) – Checks if the color is within the displayable gamut.
  • color.toGamut({ method }) – Adjusts color to fit within the gamut (clip or map).
  • color.toString({ format = "hex", method = "map" }) – Returns the color as a hex or RGB string. If the color is out of gamut, it is adjusted using the specified gamut mapping method ("map" or "clip"). "map" is used by default for perceptual accuracy.

The properties OKLab, OKLCh, and the utility function deltaEOK are not only useful for perceptual color comparison β€” they are also used internally in gamut mapping. When a color is outside the displayable sRGB gamut, Spectral.js uses OKLCh chroma reduction combined with Ξ”E optimization in OKLab space to bring the color into gamut while preserving appearance as closely as possible.


🚨 Upgrading from 2.0.2

Version 3.0 introduces breaking changes due to the addition of the Color class.

  • All functions like mix, palette, and gradient now expect Color objects, not raw arrays or hex strings.
  • Color values must be explicitly created using the Color constructor before being passed into mixing functions.

⚑ Quick Migration Example

Before (2.0.2):

let mix = spectral.mix('#002185', '#FCD200', 0.5);

Now (3.0):

let color1 = new spectral.Color('#002185');
let color2 = new spectral.Color('#FCD200');

let mix = spectral.mix([color1, 0.5], [color2, 0.5]);

πŸ‘‰ Tip: Always wrap your hex codes, RGB arrays, or CSS strings with new spectral.Color() before mixing!


🀝 Contributing

Contributions are warmly welcomed! Feel free to open an issue or submit a pull request on GitHub.


β˜• Support

If you find Spectral.js helpful, consider buying me a coffee!

ko-fi


πŸ“œ License

MIT Β© 2025 Ronald van Wijnen


🧠 Acknowledgments

  • Spectral.js is based on the Kubelka-Munk theory, developed by chemists Richard S. Kubelka and Franz Munk in the 1930s.
  • Thanks to Scott Allen Burns for his research and publication of Generating Reflectance Curves from sRGB Triplets. The spectral data Spectral.js uses is created with a variation of his LHTSS method.
  • Spectral.js uses color conversion matrices and structural inspiration from Color.js, aiming to maintain compatibility in function naming and color space representation.
  • Uses codedye for syntax highlighting on the site.
  • Thanks also to SCRT WPNS for their awesome product Mixbox which inspired me to research the Kubelka-Munk theory.
0