CIE JavaScript Tools

The Color & Illumination Engineering JavasScript toolbox, or CIE-JS in short, is a Typescript/JavasScript Library for color engineering. It can be used in online Web Applications and shell scripts, using a runtime tool such as Deno.

The color calculations in this library are different from more traditional color calculation libraries.

Firstly, it uses spectral distributions as the basis for color calculations exclusively. Colors are not represented by just three numbers, such as three R, G, and B values, but by an array of spectral power or reflectivity values. Even when you give three color values, the library will create a spectral distribution for it, based on the extra information you have, or applies a default if you don’t have any. This extra information might be that the values you have are from a camera, or that you want to display a particular color on a monitor. This makes color calculations not only more accurate but also allows you to calculate color appearance for specific observers and viewing conditions.

Secondly, the library is fully written in the Rust programming language, and made available as a WebAssembly library. You might have never heard of this language, and, if so, you can immediately forget it again, except for remembering that code written in Rust is blazingly fast, comparable to libraries written in C, and having the benefit of being very safe and reliable.

Thirdly, the whole library is written for web applications, so all the functions can be directly used in browsers, and can use the full web infrastructure. Using the library only requires adding only two lines to your scripts; there is nothing to compile or install, and runs on any modern computing platform, including mobile devices. And, if you want, you can even use it to do calculations interactively, using Deno, in your shell.

Using the library, in any browser, or any JavaScript runtime requires only two lines of additional code. In any ES6 module or script, add the following two lines at the top:

    import init, * as cie from "https://gerardharbers.com/cie.js";
    await init();

There are no extra libraries to install, and nothing to configure. In the next example, we calculate the CIELab coordinates for a Munsell color patch, using the CIE 2015 10º standard observer, instead of the more common CIE 1931 2º observer:

    const munsell = await fetchMunsell();
    const c2015_2 = await fetchCie2015_10();
    console.log(munsell.lab(c2015_2));

Now you might want to know what that color looks like on your screen. Assuming you have a display, calibrated to the sRGB color space, you can get its RGB color coordinates like this:

console.log(munsell.rgb(c2015_2));

Notice that we have not used any reference to the CIE 1931 standard observer, although the sRGB color space is defined using that observer only. This is possible because we use spectral representations of the sRGB primaries instead of their chromaticity values, as used in other libraries.

If you have measured the spectral luminance from your display with a spectrometer, you can get more precise color coordinates. If your display’s spectral data is available, for example, in a file called my_lg3420.json, you can get a better color representation using;

    const myLG = await Display.fetch("file://my_lg3420.json");
    c2015_2.display = myLG;
    console.log(munsell.rgb(c2015_2));

For more on this see the Displays Chapter.

Introduction

Illuminants

To see — and in specific to see color — we need light. During the day, when outdoors, that light is produced by the sun. In the evening light is mostly produced by electric lamps, such as incandescent, fluorescent, or LED lamps. How we see color depends on the way the objects around us are illuminated, and illuminants play an important role in colorimetry. You might have experienced “bad lighting” yourself, when buying clothes in a store, for example, and noticed that the colors of a shirt look quite different outdoors than how they looked in the store.

In colorimetry, an illuminant represents the spectral power distribution of the light illuminating an object. Typically, in colorimetric modeling, a single source of light is used, such as daylight, light from a fluorescent lamp, or LED lamps for newer installations.

The Toolbox contains mathematical illuminants, such as Blackbody, which uses Planck’s equation to calculate a spectral distribution, and illuminants represented by an array of data, which are available on this site in form of JSON data files.

All illuminants in this library implement the illuminate method, which takes a wavelength domain as an argument, and will produce an array of 64-bit floating point numbers in form of a Float64Array-type. They also implement a domain method, which produces the native wavelength domain for the data in the illuminant, or results in an undefined type, when the data has no inherent or native domain. This is for example the case with function model data, such as the Blackbody illuminant which is based on Planck’s law, or the Led illuminant, which is using a Gaussian type of function to calculate spectral distribution.

Here is an example of a Blackbody illuminant, with a color temperature of 3000K, which spectral power distribution is obtained by:


    // create a blackbody illuminant with a color temperature of 3000K
    const bb3000 = new cie.Blackbody(3000.0)

    // get its spectral power distribution in 256 points, 
    // over a wavelength range from 0.3 to 3 micrometer
    const domain = new cie.Domain(0.3E-6, 3E-6, 256);
    const spd = bb3000.illuminate(domain);

    // check first 5 values
    const want = [ 3824.39, 5545.37, 7802.22, 10684.53, 14279.18 ];
    for (const [i,w] of want.entries()) {
        assert.assertAlmostEquals(w, spd[i], 5E-3);
    }

The Blackbody illuminant is based on a mathematical model and is implemented and contained within the Cie-library. In this example, as in all the examples in this book, I have left out the common import and initialization lines, which are:

    import * as assert from "https://deno.land/std@0.156.0/testing/asserts.ts";
    import init, * as cie from "https://www.gerardharbers.com/cie.js";
    await init();

To run these examples, please add these three lines on top of your TypeScript or JavaScript file, and run them with the deno --allow-net <filename>.ts command in your terminal. I use the Deno assert functions and the Deno test features to ensure all the examples in this book work correctly; if the assert function fails, I know something is wrong.

Besides model-based illuminants, the library also has a large collection of illuminants directly defined by data. Typically these are measured directly, or derived from measured data. These illuminants are pulled into the library as a DataIlluminant, using its fetch method. In the example below we calculate the tri-stimulus values of the CIE D65 illuminant, which is pulled from the server using the fetch-API and using the standard CIE 1931 2º observer which is also fetched from the server (see Observer).


    // Get D65 illuminant using the DataIlluminant directly
    let d65 = await cie.DataIlluminant.fetch("cie/d65.json");

    // or use a convenience constructor function
    d65 = await cie.fetchD65();

    // calculate its tristimulus values using the CIE 1931 observer
    const c31 = await cie.fetchCIE1931();
    const got = d65.xyz(c31);
    const want = [95.04, 100, 108.86];
    
    for (const [i,w] of want.entries()) {
        assert.assertAlmostEquals(got[i], w,  5E-3);
    }

The data is contained in JSON files, and located on the same server as this site. To fetch these data-based illuminants, you have to use the await keyword, as they are fetched from the server asynchronously. In this library, all the functions and methods containing the word fetch need the await keyword, and if used in a function you write, it requires the keyword async too. When these async functions are invocated, they also need the await keyword. For example:

    // load the CIE 1931 standard observer, 
    // used to calculate the tri-stimulus values
    const c31 = await cie.fetchCIE1931();

    async function tristimulusValues(locs: string[]) {
        const v: number[][] = [];
        for (const loc of locs) {
            let d = await cie.DataIlluminant.fetch(loc);
            v.push(d.xyz(c31))
        }
        return v
    }

    // use await keyword here too
    const got = await tristimulusValues(
        ["cie/d50.json", "cie/d55.json", "cie/d65.json", "cie/d75.json"]
    );
    const want = [
        [ 96.42, 100, 82.50],
        [ 95.68, 100, 92.12],
        [ 95.04, 100, 108.86],
        [ 94.97, 100, 122.59]
      ];

    const got_values = got.flat();
    for (const [i,w] of want.flat().entries()) {
        assert.assertAlmostEquals(got_values[i], w,  5E-3);
    }

The library has the following illuminants and illuminant libraries (for details see the later sections in this chapter):

  • Illuminant Blackbody, using Planck’s law to calculate the spectral emission of a blackbody for a given temperature.

        const bb3000 = new cie.Blackbody(3000.0);
    
  • CIE Standard D illuminant, for correlated color temperatures in the range from 4000 to 25000K:

        const d5000 = new cie.Daylight(5000.0);
    
  • CIE Standard Data Illuminants C, D50, D55, D65, D75, F1 to F12, F3.1 to F3.15, HP1 to HP5, and LED series. For example, to get the CIE LED-B1 and the CIE F3.1 standard illuminants, use fetchLED_B1 and fetchF3_1:

        const ledB1 = await cie.fetchLED_B1();
        const f31 = await cie.fetchF3_1();
    
  • The IES TM30 illuminant collection is available in the library too. This is a collection of 318 illuminants, all defined as data illuminants in the range from 380 to 780nm with 1nm steps. This collection was used to test and optimize the TM30 and CIE color fidelity metrics. The individual illuminants can be obtained directly, using their ordinal number as listed in the TM30 Excel sheet:

        
        // get the first illuminant in this series, which is the CIE F1 illuminant;
        const f1 = await cie.DataIlluminant.fetch("tm30lib/tm30lib001.json");
    
        // illuminant 50 is a measured example of the F40T12/41U Fluorescent lamp:
        const f40t12 = await cie.DataIlluminant.fetch("tm30lib/tm30lib050.json");
    
        // get the last illuminant in this series, "Tri-band Gaussian [2)", 
        // an example of a theoretical illuminant using
        // Gaussian shaped compoents
        const tribandGauss2 = await cie.DataIlluminant.fetch("tm30lib/tm30lib318.json");
    

    Due to the size of this collection, it is also possible to search for illuminant emission types and categories, using the IlluminantsLib class methods. See for more information in the TM30 Illuminants Example library section.

  • A generic LED Array Illuminant,

Thermal Emission

All objects with a temperature greater than 0 Kelvin, and which are not perfectly reflecting, emit electromagnetic radiation. This emission of electromagnetic radiation by high temperature is called thermal emission, or incandescence.

For relatively low temperatures, say under 1500 Kelvin (≈ 1200ºC), this radiation is not directly visible; you might see the object, but only because it’s illuminated by ambient light. In the pitch dark, you wouldn’t see it all.

This all changes when an object’s temperature increases, in a pitch-dark environment. You can do this experiment if you have an old-fashioned electrical burner (the one with the metal spiral on which you put your pot), and switch it on in a completely dark room, preferably at night, and make sure you are completely dark adapted. When the temperature increases you will see initially a deep-dark red glow, which will get brighter when it heats up and changes its color too from a deep red to an orange-yellowish color later.

The emission of an object increases with the fourth power of its (absolute) temperature: $$ B(T) = \epsilon\sigma\mathrm T^4.$$ In physics, this is called the Stefan-Boltzmann law, and \(\epsilon\) is an object’s emissivity, which is 1.0 for a black object, and \(\sigma\) Stefan-Boltzmann constant [\(5.670374419\times 10^{-8}\ \mathrm{W\ m^{-2}\ K^{-4}}\)].

The color change can be characterized by the wavelength of maximum spectral emission, and this can be calculated using Wiens displacement law: $$ \lambda_{\mathrm{peak}} = \frac{b}{T},$$ with \(b\) Wien’s displacement constant [\(2.897771955\times 10^{-3} \mathrm m\ \mathrm K\)], and \(T\) the absolute temperature, in units of Kelvin, of blackbody thermal emission. Due to this spectral shift, the luminance of a hot object, which is the radiance of an object we can see, increases even faster, as the spectral emission moves to the visible part of the spectrum.

The spectral emission of hot objects, as a function of temperature, was measured in the late 1800s, and the German physicist Max Planck came up with the right model to describe it in 1900. This model is called Planck’s law, and predicts the spectral radiance \(B_\lambda\) [\(\mathrm W \mathrm{sr}^{-1} \mathrm m^{-2}\)] as a function of wavelength [\(\mathrm m\)] and temperature [\(\mathrm K\)] as: \[ B_\lambda(\lambda, T) =\frac{2hc^2}{\lambda^5}\frac 1{ \exp\left(\frac{hc}{\lambda k_\mathrm B T}\right) - 1}, \] with \(h\) Planck’s constant [\(6.62607015×10^{−34}\ \mathrm J\thinspace\mathrm{Hz}^{-1}\)] , \(c\) the speed of light [\(\mathrm m\thinspace \mathrm s^{-1}\)], and \(k_B\) Boltzmann constant [\(1.380649 × 10^{23}\ \mathrm m^2 \mathrm{kg } \thinspace\mathrm s^{-2} \mathrm K^{-1}\)].

Why is it called Blackbody emission?
Planck’s law predicts the thermal emissions spectrum for a perfectly black object or a blackbody, as it is referred to in Physics.

If an object is not black, it will reflect ambient radiation in addition to emitting thermal radiation. This makes their thermal emission very hard to measure, as even in the pitch-dark, all the objects around us will be at room temperature, and emit already a lot of radiation, which will be reflected by non-black objects. To eliminate the reflection of an object, we would need to measure it in a very cold room, or in a room with only perfectly white reflecting or silvery objects, to eliminate reflected radiation. In practice, it is easier to create a completely black object, which is typically done by making an almost completely enclosed oven, coated with black ceramic tiles on the inside, and with a small opening through which the black-body radiation is emitted and measured.

The reflectivity of objects, or its complement emissivity, is typically material-dependent and varies with wavelength and temperature; there are no general mathematical models for predicting the reflectivity of materials, similar to Planck’s law, and it has to be measured.

Sometimes people use the term Planckian emission, instead of Blackbody emission. This name refers to thermal emission’s model, and not its physics phenomenon, and would be similar to referring to gravity as “Newtonian attraction”. If a particular subject or item directly refers to Planck’s law, it may make sense to refer to it as “Planckian”. An example would be the line in a chromaticity diagram of coordinates of light emitted by a black object; such a line is often called Planckian Locus and refers to the model used to calculate the line, not the physics phenomenon.

How hot can objects get?
Most materials will burn or melt before they start to emit light. Tungsten is a metal with the highest melting point we know and is a good material for generating thermal emissions. A tungsten filament can emit bright white light when placed in a lightbulb, depleted from any oxygen. It can be heated by running a current through it.

For non-metal objects, or plasma and gasses, temperatures and Radiant Emittance can become so high that they can damage the eyes. The emission of a welding arc, or the emission of our Sun, are examples of this, which both require very dark protective screens when looking at them.

Blackbody Illuminant

The library’s Blackbody illuminant class implements the equations given above. Peak wavelengths and CIE 1931 chromaticity coordinates of a blackbody thermal emitter, for example, in the range from 2750 to 6500K in 250K steps, can be calculated using the following:


    // load CIE 1931 standard observer
    const c31 = await cie.fetchCIE1931();

    const output: string[] = [];
    for (let t=2750.0; t<=6500.0; t+=250.0) {
        const bb = new cie.Blackbody(t);
        const peak = bb.peakWavelength * 1.0E9;
        const [_, x, y] = bb.lxy(c31);
        output.push(
            `<tr>
            <td>${t}K</td>
            <td>${peak.toFixed(1)}nm</td>
            <td>${x.toFixed(4)}</td>
            <td>${y.toFixed(4)}</td>
            </tr>`);
    }
    
    // check first two lines
    const want = [
        "<tr><td>2750K</td><td>1053.7nm</td><td>0.4558</td><td>0.4097</td></tr>",
        "<tr><td>3000K</td><td>965.9nm</td><td>0.4369</td><td>0.4041</td></tr>"
    ];
    for (const [i,w] of want.entries()) {
        // remove all white space in output before comparison
        assert.assertEquals(output[i].replace(/\s+/g,''), w);
    }
}) 

Its output is formatted as rows of an HTML table and is included in the table below.

TemperatureWavelength
Peak
CIE 1931 xCIE 1931 y
2750K1053.7nm0.45580.4097
3000K965.9nm0.43690.4041
3250K891.6nm0.42020.3976
3500K827.9nm0.40530.3907
3750K772.7nm0.39210.3837
4000K724.4nm0.38050.3768
4250K681.8nm0.37010.3700
4500K643.9nm0.36080.3636
4750K610.1nm0.35250.3574
5000K579.6nm0.34510.3516
5250K552.0nm0.33850.3462
5500K526.9nm0.33250.3411
5750K504.0nm0.32700.3363
6000K483.0nm0.32210.3318
6250K463.6nm0.31760.3276
6500K445.8nm0.31350.3237

Irradiance and Luminous Efficacy of Thermal Radiation

The total power for blackbody thermal emission of an object is given by Stefan-Boltzmann law and is used in the Blackbody class. It is used to calculate the radiant emittance and irradiance of a blackbody radiator. In this library, where Blackbody is used as an illuminant, irradiance is set to 1 Watt per square meter by default. It can be set by direct assignment; in this example, we set it to 100 Watt per square meter:

    // create a blackbody illuminant, with a temperature of 3000K
    const bb = new cie.Blackbody(3500.0);

    // set irradiance to 100W per square meter
    bb.irradiance  = 100.0;

Blackbody radiation has a very broad emission spectrum, ranging far in the infrared: to calculate the total power in this example, the wavelength domain is set from 200 nanometers to 50 micrometers, to capture the output completely:

    // get spectral irradiance distribution over a wavelength range
    // from 200 nanometer to 50 micrometer, with 1000 points
    const domain = new cie.Domain(200E-9, 50E-6, 1000);
    const sid = bb.illuminate(domain);

    // Confirm irradiance in this spectrum to be 100.0
    // by integration with Trapezoidal rule
    const calcIrr= cie.integrate(sid, domain.step());
    assert.assertAlmostEquals(calcIrr, 100.00, 5E-3);

Adding the following lines to the previous script reveals that the total power of a blackbody radiator with a color temperature of 3500 Kelvin in the visible part of the spectrum — here represented by a wavelength domain from 380 to 780 nanometers with 1-nanometer steps — is only 20.69%.


    // Confirm irradiance in the visible part of the spectrum to be 20.69W/m2,
    // or 20.69%, as we have set the irradiance to 100W/m2.
    const domainVis = new cie.Domain(380E-9, 780E-9, 401);
    const sidVis = bb.illuminate(domainVis);
    const powVis = cie.integrate(sidVis, domainVis.step());

    assert.assertAlmostEquals(powVis, 20.69, 5E-3);

Here is another example, where we calculate the Luminous Equivalent of Radiation, or LER, for blackbody emission, for a range from 2000 to 3500 Kelvin, in steps of 250 Kelvin:


    const c31 = await cie.fetchCIE1931();

    const output:string[] = [];
    for (let t = 2000.0; t<=3500.0; t+=250.0) {
        const bb = new cie.Blackbody(t);
        const lm = bb.illuminance(c31);
        output.push(`<tr>
            <td>${t.toFixed()}K</td>
            <td>${lm.toFixed(1)}lm/W</td>
        </tr>`.replace(/\s+/g,''));
    }

    const want = [
        "<tr><td>2000K</td><td>1.6lm/W</td></tr>",
        "<tr><td>2250K</td><td>4.0lm/W</td></tr>",
        "<tr><td>2500K</td><td>8.0lm/W</td></tr>",
        "<tr><td>2750K</td><td>13.6lm/W</td></tr>",
        "<tr><td>3000K</td><td>20.7lm/W</td></tr>",
        "<tr><td>3250K</td><td>28.8lm/W</td></tr>",
        "<tr><td>3500K</td><td>37.4lm/W</td></tr>"
    ];
    for (const [i, w] of want.entries()) {
        assert.assertEquals(output[i], w);
    }

The output of this script shows that the Luminous Efficacy of Radiation for thermal emission is strongly dependent on temperature:

TemperatureLER
2000K 1.6lm/W
2250K 4.0lm/W
2500K 8.0lm/W
2750K 13.6lm/W
3000K 20.7lm/W
3250K 28.8lm/W
3500K 37.4lm/W

Used as lamps, intended to produce light — defined as electromagnetic radiation we can see — blackbody emitters are not very efficient. Most of the power of an incandescent lamp is emitted as infrared radiation, and not as light. LED and fluorescent lamps, optimized to emit light only and render color with high fidelity, achieve LER values in the range from 300 to 350 lumens per watt; see for more on color rendering in the sections on Color Rendering Index (CRI) and Color Fidelity Index (CFI).

The Luminous Efficacy of Radiation (LER) is different from the Luminous Efficacy of a light source or lighting system (LES). Luminous Efficacy of Radiation is the luminous flux in a beam of light divided by its radiant power, while Luminous Efficacy is the total luminous flux emitted by a source or a lighting system, divided by its power input. To use an example of the difference: you can calculate the Luminous Efficacy of Radiation of sunlight using its spectral distribution, but it is impossible to calculate its Luminous Efficacy, as long as you don’t know the Sun’s total luminous flux output and find a way to calculate its power input.

Second Radiative Constant C2

Planck’s law can also be written using the radiative constants c1 and c2, in this example describing spectral radiant exitance \(M\): $$M(\lambda,T) =\frac{c_{1}}{\lambda^5}\frac{1}{\exp\left(\frac{c_2}{\lambda T}\right)-1}$$ with the first radiative constant defined as: $$c_1 = 2\pi h c^2,$$ and the second radiative constant defined as: $$c_2 = \frac{h c}{k_B},$$ and with \(h\) Planck’s constant, \(c\) the speed of light, and \(k_B\) Boltzmann’s constant. With changes in the definition of fundamental constants over time, the radiative constants changed a bit. Changes in the first constant changed the output as described by Planck’s law which in general is often not a critical parameter. However, changes in radiative constant c2 changed the international temperature scale too, and definitions of standard illuminants in colorimetry.

For example, the standard CIE illuminant D65, intended to represent daylight with a correlated color temperature of 6500 K, and specified by the CIE as a table of spectral values, was calculated in 1967 with a value of \(c_2\) of $$ c_2=1.438 0 \times 10^{-2} \text{m·K}.$$ The current value, as of 2020, is: $$ c_2=1.438 776 877… \times 10^{-2} \text{m·K}.$$ The effect of this redefinition is that the international temperature scale has changed. For example, instead of using a value of 6500 K as a correlated color temperature for the D65 illuminant, a value of $$ T = \frac{c_2}{c_{2,\text{1948}}}\times 6500.0 = 6503.51 $$ has to be used to calculate the spectral distribution values of the D65 illuminant, which is still the recommended illuminant used in many standards.

The temperature scale change also affected illuminants D50, D55, and D75. The new values for the Dxx-illuminants can be calculated like this:


    const c2 = cie.c2Value(cie.PlanckC2.Exact);
    const c2_48 = cie.c2Value(cie.PlanckC2.Ipts1948);
    const want = [
        "<tr><td>5000.00</td><td>5002.70</td></tr>",
        "<tr><td>5500.00</td><td>5502.97</td></tr>",
        "<tr><td>6500.00</td><td>6503.51</td></tr>",
        "<tr><td>7500.00</td><td>7504.05</td></tr>"
    ]

    for (const [i,t_prev] of [5000.0, 5500.0, 6500.0, 7500.0].entries()) {
        const t_now = c2/c2_48 * t_prev;
        const got = `<tr>
            <td>${t_prev.toFixed(2)}</td>
            <td>${t_now.toFixed(2)}</td>
        </tr>`;
        // remove white space in string
        assert.assertEquals(got.replace(/\s+/g,""), want[i]);
    }

The output here is are rows of data, which are included in the following table:

Temperature
1967
Temperature
current
5000.005002.70
5500.005502.97
6500.006503.51
7500.007504.05

The radiative constant c2 can also be directly set in a Blackbody instance, using the PlanckC2 enum type:

  • PlanckC2.Exact, the current and exact values defined by SI base units,
  • PlanckC2.Nbs1931, set by the US National Bureau of Standards in 1931, and used to calculate the spectral distribution of the A illuminant,
  • PlanckC2.Ipts1948, value as used in the International Practical Temperature Scale in 1948, and used in the definition of the Dxx-illuminants,
  • PlanckC2.Its1968, value as set in the Internation Temperature Scale in 1968.

And here is a, somewhat contrived example, in which we calculate the chromaticity coordinates of CIE standard A Illuminant, which had at its time of definition, a color temperature of 2848 Kelvin. The standard A illuminant’s spectrum is calculated using Planck’s law, using the CIE 1931 NBS value for c2. In this example, we confirm that the chromaticity coordinates of the A illuminant calculated this way, are the same as those obtained with the current c2 definition, but using a temperature of 2856K.


    const aNbs = new cie.Blackbody(2848.0);
    aNbs.c2 = cie.PlanckC2.Nbs1931;

    const c31 = await cie.fetchCIE1931();

    // A-Illuminant CIE 1931 Chromaticity values (CIE.14.2004)
    const want = "[0.447576,0.407448]";

    // A-Illuminant at 2848.0K using NBS 1931 temperature scale
    const [_l1, x1, y1] = aNbs.lxy(c31);
    const got =`[${x1.toFixed(6)},${y1.toFixed(6)}]`;
    assert.assertEquals(got, want);

    // A-Illuminant at 2848.0K using the exact temperature scale
    const tNew = cie.c2Value(
        cie.PlanckC2.Exact)/cie.c2Value(cie.PlanckC2.Nbs1931) * 2848.0;
    const aNow = new cie.Blackbody(tNew);
    const [_l2, x2, y2] = aNow.lxy(c31);
    const got2 =`[${x2.toFixed(6)},${y2.toFixed(6)}]`;
    assert.assertEquals(got2, want);

Incandescent Lamps

Incandescent lamps typically have Tungsten filaments —Tungsten has the highest melting point of all metal elements— placed within a glass envelope, or the bulb, filled with a gas, such as argon, krypton, or nitrogen, but without oxygen. The emission of these lamps is different from a blackbody radiator, as the Tungsten element is partially reflective, and its emissivity is dependent on wavelength and temperature. In addition, part of the light emitted in a coiled filament is reflected, which alters its spectral distribution. And the glass envelope is also not completely transparent and has often a small tint which affects the spectral distribution of its output too. And last but not least, incandescent lamps are typically placed into a lighting fixture, which can have reflectors, diffusers, and even filters in form of lampshades, which tend to completely change their spectral illumination properties.

Incandescent lamps are getting more and more replaced with so-called “LED Retrofit lamps”, which have more or less the same shape as the traditional incandescent lamp, which come with a much larger variety with regards to color temperature, from warm-white to cool-white, larger variation in color rendering properties, and, due physics principles and driver constraints, much higher ‘flicker’. More on that in the LED Lamp section. Incandescent filament lamp output varies with filament temperature, which tends to vary only a little when driven at 50 or 60 Hz AC drive currents.

Incandescent-Filament Lamp NameLocation
Halogen [1)tm30/tm30lib076.json
Halogen [2)tm30/tm30lib077.json
Halogen [3)tm30/tm30lib078.json
Halogen MR16 [1)tm30/tm30lib079.json
Halogen MR16 [2)tm30/tm30lib080.json
Halogen MR16 [3)tm30/tm30lib081.json
Incandescent [60WA19)tm30/tm30lib082.json
Incandescent [75WA19 Halogena)tm30/tm30lib083.json
Incandescent [75WA19 Neodymium)tm30/tm30lib084.json
Incandescent [75WA19 Rough House)tm30/tm30lib085.json
Incandescent [75WA19 Softer White)tm30/tm30lib086.json
Krypton Incandescenttm30/tm30lib087.json
Neodymium Incandescenttm30/tm30lib088.json
Filtered Halogentm30/tm30lib089.json

The TM30 example library has a small collection of measured incandescent filament lamp examples, as shown above; it was generated using the script below:

    import init, {
        IlluminantsLib, EmissionType, IlluminantCategory
        } from "https://www.gerardharbers.com/cie.js";
    await init();

    const index = await IlluminantsLib.fetch("tm30", 
        EmissionType.IncandescentFilament, IlluminantCategory.All);
    const data = new Map([...index.index()].sort());

    // print out as html table
    console.log("<table><tr><th span=2>Incandenscent Data Illuminants</th></tr>");
    for (const [k,v] of data) {
        console.log(`<tr><td>${v.name}</td><td>${k}</td></tr>`);
    }
    console.log("</table>")

/*
    deno run --allow-net examples/illuminants/tm30/incandescent.ts
    <table><tr><th>Name</th><th>Location</tr>
    <tr><td>Halogen [1)</td><td>tm30/tm30lib076.json</td></tr>
    ...
*/

These illuminants can be used with the DataIlluminant.fetch method; for example, to get the CIE 1931 chromaticity coordinates of a 60W ‘regular’ lightbulb, use:

    import init, {
    DataIlluminant, fetchCIE1931, AppearanceModel
        } from "https://www.gerardharbers.com/cie.js";
    await init();

    const a19Illuminant = await DataIlluminant.fetch("tm30/tm30lib082.json");
    const c31 = await fetchCIE1931();

    const [l, x, y] = new AppearanceModel(c31, a19Illuminant).lxyRef();
    console.log(a19Illuminant.name, l.toFixed(), x.toFixed(5), y.toFixed(5));
    /*
        deno run --allow-net examples/illuminants/tm30/a60w.ts
        Incandescent [60WA19) 100 0.45072 0.40803
    */
```ts

CIE Daylight

TM30 Illuminant Examples

LED Illuminants

Direct LED emission, as opposed to Phosphor-Converted LED emission, can be described using Gaussian-based spectral irradiance distributions. Red LEDs typically use an AlInGaP semiconductor material, which has a red emission, and blue and green LEDs typically use an InGaN semiconductor. White light can be generated by a combination of at least three red, green, blue, and amber LEDs, or by using a blue or violet emitting LED chip, in combination with a phosphor mixture, which converts part of the emission to yellowish-green light. There is no phosphor-converted white LED model in this library, due to the complex spectral interactions of the phosphor materials.

Direct emission of InGaN and AlInGaP LEDs are implemented in this library and are modeled in the LedArray illuminant class. The spectral model used is described by Y. Ohno in Spectral design considerations for white LED color rendering, in which the spectral distribution \(S_i\) of an individual LED emitter is given by: $$ S(\lambda) = \frac{g(\lambda) + 2 g^5(\lambda)}{3}.$$ The function \(g(\lambda)\) a Gaussian function, centered around a center wavelength \(\lambda_c\), and with a width of \(\lambda_w\): $$ g(\lambda) = e^{-((\lambda-\lambda_c)/\lambda_w)^2}.$$ The spectral width here is not exactly the full half maximum width, for both the \(g(\lambda)\) and \(S(\lambda)\) functions. For the single LED irradiance distribution a full half maximum width of \(\lambda_2\) as input a value for\(\lambda_w\) is used to obtain the correct full width half maximum width: $$\lambda_w = 1.08480681239\ldots\times\lambda_2.$$

The irradiance of an individual LED is given by: $$ \int_{-\infty}^\infty S(\lambda) d\lambda = a\lambda_{w},$$ with the constant \(a\) having a value of: $$ a = \frac{5 + 2\sqrt{5}}{15}\sqrt{\pi} = 1.11926\ldots.$$ The total spectral irradiance distribution from an array is obtained by summation of the individual LED spectral irradiances, \(S_i\), normalized to have an irradiance of 1.0 Watt per square meter, and scaled by their target irradiance \(E_{e,i}\): $$ \overline{S(\lambda)} = \sum_i E_{e,i}\frac{S_i(\lambda)}{a\lambda_{w,i}}.$$ Total irradiance of this composite spectral distribution is simply given by: $$ \overline{E_e} = \sum_i E_{e,i}.$$

LED Array Illuminant

In a LedArray, each LED in the array is described by an irradiance parameter \(E_e\) (in units of watts per square meter), a center wavelength \(\lambda_c\) (in units of meters), and its full width at half maximum value \(\lambda_2\) (also in units of meters). The illuminant requires at least a single LED, with its parameters supplied in the constructor.

In this example we start with a single LED, with an irradiant power of 4 W/m2, a center wavelength of 450 nm (450E-9 m), and a width of 25 nm (25E-9 m):

        const ledArray = new cie.LedArray(4.0, 450E-9, 30E-9);

And we can add more LEDs, with center wavelengths of 550 and 650 nm, and spectral widths of 40 and 20 nm, respectively, by using the add_led method:

        ledArray.addLed(6.0, 550E-9, 40E-9);
        ledArray.addLed(3.0, 650E-9, 20E-9);

Using its illuminate method, we get a spectral irradiance distribution over a wavelength range from 380 to 780 nm, with a step size of 1 nm. And to confirm that the total irradiance is the sum of the LED’s irradiances, we can integrate the spectral distribution, and check it is 13 W/m2, which is the sum of the powers we used in the LedArray constructor:

        const domain = new cie.Domain(380E-9, 780E-9, 401);
        const sid = ledArray.illuminate(domain);
        const totalIrradiance = cie.integrate(sid,domain.step());
        assert.assertAlmostEquals(totalIrradiance, 13.00, 5E-3);

To change the total target irradiance from an LED array, you can set its irradiance value. Here we set it to a value of 10W/m2, and confirm it worked, by integrating all the values of the spectral irradiance distribution:

        ledArray.irradiance = 10.0;
        const newIrr = cie.integrate(ledArray.illuminate(domain), domain.step());
        assert.assertAlmostEquals(newIrr, 10.00, 5E-3);

Total irradiance is different from illuminance. You cannot directly set the target illuminance in the LedArray class, but you can use its scale function after calculating its illuminance after defining an observer using this illuminant. Here we set it to 10W/m2, and confirm it worked, by integrating all the values of the spectral irradiance distribution:

        // need an observer to calculate photometric and colorimetric vales
        const c31 = await cie.fetchCIE1931();
        const ill = ledArray.illuminance(c31);
        ledArray.scale(5000.0/ill);
        const newIll = ledArray.illuminance(c31);
        assert.assertAlmostEquals(newIll, 5000.00, 5E-3);

Target White Point

The LEDArrayIlluminant class requires the exact irradiance values to construct or add LEDs. If you don’t know the exact component irradiance values to target a particular white point, you can use the LEDArrayIlluminantFactory function, which requires a color-matching function to use, a target illuminant, and a flat array of center and wavelength pairs, without a power specification:

Colorants

Observers