@magmacrunch/adenosine-spectrum 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API.md ADDED
@@ -0,0 +1,236 @@
1
+ # API Reference: adenosine-spectrum
2
+
3
+ Colour science for light. Every function is pure and every spectrum is in
4
+ nanometres. Global `AdSpectrum` in the IIFE build.
5
+
6
+ ## Table of Contents
7
+
8
+ - [Types](#types)
9
+ - [Spectra](#spectra)
10
+ - [Physics](#physics)
11
+ - [Colorimetry](#colorimetry)
12
+ - [OKLab](#oklab)
13
+ - [sRGB](#srgb)
14
+ - [Output](#output)
15
+
16
+ ---
17
+
18
+ ## Types
19
+
20
+ | Type | Shape |
21
+ |------|-------|
22
+ | `Spectrum` | `{ start, step, values }`: `values[i]` is the value at `start + i * step` nm |
23
+ | `SpectrumLike` | a `Spectrum`, or a function from nm to a value |
24
+ | `XYZ` | `[X, Y, Z]`, CIE tristimulus |
25
+ | `RGB` | `[r, g, b]`, linear or encoded depending on the function, 0..1 in gamut |
26
+ | `Oklab` | `[L, a, b]`, perceptual lightness 0..1 and two opponent axes |
27
+ | `Gamut` | `'perceptual'` (default), `'desaturate'`, `'clip'` or `'none'` |
28
+
29
+ `CMF_RANGE` is `{ start: 360, end: 830 }`, the matching functions' range and
30
+ the default sampling range.
31
+
32
+ ---
33
+
34
+ ## Spectra
35
+
36
+ ### `spectrum(start, step, values)`
37
+
38
+ Builds a sampled spectrum. Copies `values`. Throws `RangeError` for a step that
39
+ is not positive or an empty array.
40
+
41
+ ### `sample(fn, start?, end?, step?)`
42
+
43
+ Samples a function of wavelength onto a uniform grid, both ends included.
44
+ Defaults to 360-830 nm at 1 nm.
45
+
46
+ ### `valueAt(s, nm)`
47
+
48
+ The value at a wavelength. Linear interpolation between samples, zero outside
49
+ the range. A function is simply called.
50
+
51
+ ### `multiply(a, ...others)`
52
+
53
+ Multiplies spectra together on the grid of `a`. This is how a filter, a
54
+ reflectance or a transmission is applied to a light. `others` may be spectra or
55
+ functions.
56
+
57
+ ```js
58
+ const ground = multiply(blackbody(5778), rayleigh(0.1));
59
+ ```
60
+
61
+ ### `scale(s, k)`
62
+
63
+ Multiplies every sample by `k`.
64
+
65
+ ### `normalize(s)`
66
+
67
+ Scales so the largest sample is 1. An all-zero spectrum comes back unchanged.
68
+
69
+ ---
70
+
71
+ ## Physics
72
+
73
+ ### `planck(nm, kelvin)`
74
+
75
+ Planck's law: blackbody spectral radiance in W sr⁻¹ m⁻² nm⁻¹, with the exact
76
+ CODATA 2018 constants.
77
+
78
+ ### `wienPeak(kelvin)`
79
+
80
+ The wavelength in nm at which a blackbody's spectral radiance peaks. 501.5 nm
81
+ for the Sun's 5778 K.
82
+
83
+ ### `blackbody(kelvin, start?, end?, step?)`
84
+
85
+ A blackbody's relative spectrum, 1 at 560 nm. Defaults to 360-830 nm at 1 nm.
86
+
87
+ ### `illuminantD65()`
88
+
89
+ CIE standard illuminant D65, average daylight, from the CIE table: 300-780 nm
90
+ at 5 nm, 100 at 560 nm. Zero beyond 780 nm, where the matching functions carry
91
+ almost nothing.
92
+
93
+ ### `illuminantA()`
94
+
95
+ CIE standard illuminant A, tungsten, from the CIE's defining formula:
96
+ 300-830 nm at 1 nm, 100 at 560 nm.
97
+
98
+ ### `rayleigh(opticalDepth, referenceNm?)`
99
+
100
+ A transmission function for a Rayleigh-scattering layer: the fraction of light
101
+ left after optical depth τ at the reference wavelength (550 nm by default),
102
+ scaling as the inverse fourth power of wavelength. Clear air at zenith is about
103
+ τ = 0.1 at 550 nm.
104
+
105
+ ---
106
+
107
+ ## Colorimetry
108
+
109
+ ### `cmf(nm)`
110
+
111
+ The CIE 1931 2° colour-matching functions `[x̄, ȳ, z̄]` at a wavelength, from
112
+ the 1 nm table. Interpolated linearly, zero outside 360-830 nm. ȳ is 1 at
113
+ 555 nm.
114
+
115
+ ### `toXYZ(s, opts?)`
116
+
117
+ CIE XYZ of a spectrum, integrated against the 1931 observer at 1 nm.
118
+
119
+ | Param | Type | Default | Description |
120
+ |-------|------|---------|-------------|
121
+ | `s` | `SpectrumLike` | | the light, or with an illuminant, a reflectance 0..1 |
122
+ | `opts.illuminant` | `SpectrumLike` | | when given, `s` is a reflectance and Y = 1 for a perfect reflector |
123
+
124
+ With no illuminant the result is the raw integral, in the units of `s` times nm.
125
+ Throws `RangeError` for an illuminant with no luminance.
126
+
127
+ ```js
128
+ const grey = toXYZ(() => 0.5, { illuminant: illuminantD65() }); // Y = 0.5
129
+ ```
130
+
131
+ ### `chromaticity(xyz)`
132
+
133
+ CIE xy. Black returns the D65 white point.
134
+
135
+ ---
136
+
137
+ ## OKLab
138
+
139
+ Björn Ottosson's perceptual colour space (2020): lightness L from 0 to 1, and
140
+ `a`, `b` whose angle is hue and whose length is chroma. The gamut mapping
141
+ below works in it. All four accept out-of-gamut and negative values.
142
+
143
+ ### `xyzToOklab(xyz)` / `oklabToXyz(lab)`
144
+
145
+ CIE XYZ (D65, Y = 1 for white) to OKLab and back, with Ottosson's matrices.
146
+
147
+ ### `linearSrgbToOklab(rgb)` / `oklabToLinearSrgb(lab)`
148
+
149
+ The same, from and to linear sRGB.
150
+
151
+ ---
152
+
153
+ ## sRGB
154
+
155
+ ### `xyzToLinearSrgb(xyz)`
156
+
157
+ XYZ (D65, Y = 1 for white) to linear sRGB with the IEC 61966-2-1 matrix. Out of
158
+ gamut gives channels outside 0..1.
159
+
160
+ ### `linearSrgbToXyz(rgb)`
161
+
162
+ The inverse.
163
+
164
+ ### `encodeSrgb(v)` / `decodeSrgb(v)`
165
+
166
+ The sRGB transfer function and its inverse, per channel.
167
+
168
+ ### `xyzToSrgb(xyz, opts?)`
169
+
170
+ XYZ to encoded sRGB.
171
+
172
+ | Param | Type | Default | Description |
173
+ |-------|------|---------|-------------|
174
+ | `opts.gamut` | `Gamut` | `'perceptual'` | how an out-of-gamut colour is brought inside |
175
+
176
+ | Gamut | What it does | Keeps |
177
+ |---|---|---|
178
+ | `'perceptual'` | reduces OKLab chroma at constant lightness and hue until the colour fits, by bisection; lightness past white gives white, below black gives black | perceived hue and lightness |
179
+ | `'desaturate'` | mixes toward the grey of the same luminance until no channel is negative, then scales down if any exceeds 1 | dominant wavelength, which is not perceived hue: a deep spectral red lands on the red-blue edge and reads pink |
180
+ | `'clip'` | clamps each channel to 0..1 | nothing in particular; cheap |
181
+ | `'none'` | returns what the matrix gives, negatives included | |
182
+
183
+ ### `lightColor(s, opts?)`
184
+
185
+ The colour of a light at full brightness: its chromaticity, scaled so the
186
+ brightest channel is 1, encoded. Intensity is discarded, so a spectrum and any
187
+ multiple of it give the same colour. Black for a spectrum with no luminance.
188
+
189
+ | Param | Type | Default | Description |
190
+ |-------|------|---------|-------------|
191
+ | `opts.gamut` | `Gamut` | `'perceptual'` | as for `xyzToSrgb` |
192
+
193
+ A light inside sRGB is scaled so its brightest channel is 1. Under
194
+ `'perceptual'`, a light outside it becomes the most chromatic sRGB colour of
195
+ the same OKLab hue, the cusp, which is where the in-gamut case arrives at the
196
+ gamut's edge, so the two meet without a step.
197
+
198
+ ```js
199
+ lightColor(blackbody(5778)); // the Sun from space: slightly warm of white
200
+ lightColor(illuminantD65()); // [1, 1, 1]
201
+ ```
202
+
203
+ ### `wavelengthColor(nm, opts?)`
204
+
205
+ Monochromatic light at a wavelength as displayable sRGB. Every spectral colour
206
+ is outside sRGB; under the default `'perceptual'` mapping each wavelength gets
207
+ the most chromatic sRGB colour of its OKLab hue. Black outside 360-830 nm.
208
+
209
+ What that looks like, and why: at full brightness the two ends of the strip are
210
+ flat, because 380-420 nm share one hue to the eye and so do 640-700 nm; the
211
+ blue to cyan stretch changes fast, because hue does there, and passes through
212
+ sRGB blue near 459.5 nm. OKLab puts deep spectral red a few degrees toward blue
213
+ of the sRGB red primary, so reds carry a little blue. `brightness: 'luminous'`
214
+ is the more natural strip: the flat ends fade, as they do to the eye.
215
+
216
+ | Param | Type | Default | Description |
217
+ |-------|------|---------|-------------|
218
+ | `opts.gamut` | `Gamut` | `'perceptual'` | as for `xyzToSrgb` |
219
+ | `opts.brightness` | `'full' \| 'luminous'` | `'full'` | `'luminous'` dims each wavelength by ȳ, so the ends fade as they do to the eye |
220
+
221
+ ---
222
+
223
+ ## Output
224
+
225
+ ### `to8bit(rgb)`
226
+
227
+ Encoded 0..1 channels to 0..255 integers, clamped.
228
+
229
+ ### `toCss(rgb, alpha?)`
230
+
231
+ Encoded channels to a CSS colour string: the `rgb` form, or the `rgba` form
232
+ when `alpha` is given, with 0..255 integer channels.
233
+
234
+ ```js
235
+ ctx.fillStyle = toCss(wavelengthColor(550));
236
+ ```
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog: `@magmacrunch/adenosine-spectrum`
2
+
3
+ This package only. Repo-wide changes, such as CI, guard scripts and build
4
+ tooling, live in the [monorepo changelog](../../CHANGELOG.md).
5
+
6
+ The packages version independently, so a number here means nothing about any
7
+ other package.
8
+
9
+ ## Unreleased
10
+
11
+ _Nothing yet._
12
+
13
+ ## 0.1.0 - 2026-10-17
14
+
15
+ First release. Colour science for light, lifted out of the music video
16
+ *sunlight is more green than any other color* and made exact:
17
+
18
+ - The CIE 1931 2° colour-matching functions at 1 nm, from the CIE table rather
19
+ than the piecewise-Gaussian fit the video used, which is good to a few
20
+ percent and no better.
21
+ - `planck` with exact CODATA constants, `blackbody`, `wienPeak`, and the
22
+ D65 and A illuminants.
23
+ - `rayleigh`, a transmission function, and `multiply` to apply it or any
24
+ filter or reflectance.
25
+ - `toXYZ`, emissive or reflective under an illuminant, and `chromaticity`.
26
+ - OKLab both ways, from XYZ and from linear sRGB.
27
+ - `lightColor` and `wavelengthColor`, and a `'perceptual'` gamut mapping as
28
+ the default everywhere: OKLab chroma reduced at constant lightness and hue,
29
+ and for full-brightness colours the most chromatic sRGB colour of the same
30
+ hue. `'desaturate'` (toward the grey of the same luminance, keeping dominant
31
+ wavelength), `'clip'` and `'none'` remain as options.
32
+ - Held by tests to reference values from colour-science, which
33
+ `tools/generate.py` computes and also writes the tables from.
package/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Required Notice: Copyright 2026 magmacrunch media (https://magmacrunch.com)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
package/NOTICE ADDED
@@ -0,0 +1,18 @@
1
+ Adenosine
2
+ Copyright 2026 magmacrunch media
3
+
4
+ Required Notice: Copyright 2026 magmacrunch media (https://magmacrunch.com)
5
+
6
+ Licensed under the PolyForm Noncommercial License 1.0.0, in the LICENSE file
7
+ beside this one. Any noncommercial purpose is permitted, personal projects,
8
+ teaching and research included. Commercial use is not; for a commercial
9
+ licence, ask.
10
+
11
+ The licence asks that the "Required Notice" line above travels with any copy
12
+ you pass on. That is how credit for adenosine reaches people downstream.
13
+
14
+ Versions published under Apache-2.0, up to 2026-10-15, stay under Apache-2.0
15
+ for anyone who already has them. These terms govern this version onward.
16
+
17
+ Adenosine is a collection of lightweight web game engines:
18
+ https://magmacrunch.com/ware/adenosine/
package/README.md CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # @magmacrunch/adenosine-spectrum
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Colour science for light: the CIE 1931 2° colour-matching functions at 1 nm,
4
+ Planck blackbodies, the D65 and A illuminants, Rayleigh scattering, and
5
+ spectrum to sRGB with a perceptual gamut mapping (OKLab chroma reduction at
6
+ constant lightness and hue). For anything that needs
7
+ the colour of a light rather than a colour picked by eye. No runtime
8
+ dependencies.
9
+
10
+ ```bash
11
+ npm install @magmacrunch/adenosine-spectrum
12
+ ```
13
+
14
+ ## Use
15
+
16
+ ```js
17
+ import {
18
+ blackbody, multiply, rayleigh, lightColor, wavelengthColor, toCss,
19
+ } from '@magmacrunch/adenosine-spectrum';
20
+
21
+ // The Sun from space, and after a thick slab of air
22
+ const sun = blackbody(5778);
23
+ ctx.fillStyle = toCss(lightColor(sun));
24
+ ctx.fillStyle = toCss(lightColor(multiply(sun, rayleigh(1))));
25
+
26
+ // A spectrum strip, ends fading the way they do to the eye
27
+ for (let nm = 380; nm <= 700; nm++) {
28
+ ctx.fillStyle = toCss(wavelengthColor(nm, { brightness: 'luminous' }));
29
+ ctx.fillRect(nm - 380, 0, 1, 20);
30
+ }
31
+ ```
32
+
33
+ From a `<script>` tag, everything is on `window.AdSpectrum`:
34
+
35
+ ```html
36
+ <script src="https://cdn.jsdelivr.net/npm/@magmacrunch/adenosine-spectrum@0.1/dist/index.global.js"></script>
37
+ ```
38
+
39
+ A spectrum is either a sampled `{ start, step, values }` in nm or a plain
40
+ function of nm, and the two mix freely: `multiply(blackbody(5778), (nm) => 0.5)`
41
+ works.
42
+
43
+ ## Accuracy
44
+
45
+ The tables are the CIE's own, as distributed by
46
+ [colour-science](https://www.colour-science.org/): the 1931 2° observer at
47
+ 1 nm from 360 to 830 nm, and D65 at 5 nm. The test suite holds the package to
48
+ reference values colour-science computes, by `tools/generate.py`, which is also
49
+ what writes `src/data.ts`: blackbody chromaticities from 1000 K to 20000 K to
50
+ 2×10⁻⁶ in xy, reflectances under D65 to 10⁻⁶ in XYZ, and the published D65 and
51
+ A white points to 10⁻⁴.
52
+
53
+ What this is not: a colour-appearance model, a chromatic adaptation transform
54
+ or a wide-gamut pipeline. Output is sRGB, D65 white, for a screen.
55
+
56
+ See [API.md](API.md) for every function.
57
+
58
+ ## Licence
59
+
60
+ PolyForm Noncommercial 1.0.0. See `LICENSE` and `NOTICE`.
package/dist/data.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type Matrix = readonly (readonly [number, number, number])[];
2
+ export declare const CMF_START = 360;
3
+ export declare const CMF_STEP = 1;
4
+ export declare const CMF_X: readonly number[];
5
+ export declare const CMF_Y: readonly number[];
6
+ export declare const CMF_Z: readonly number[];
7
+ export declare const D65_START = 300;
8
+ export declare const D65_STEP = 5;
9
+ export declare const D65_VALUES: readonly number[];
10
+ export declare const XYZ_TO_LINEAR_SRGB: Matrix;
11
+ export declare const LINEAR_SRGB_TO_XYZ: Matrix;
12
+ export declare const OKLAB_M1: Matrix;
13
+ export declare const OKLAB_M1_INV: Matrix;
14
+ export declare const OKLAB_M2: Matrix;
15
+ export declare const OKLAB_M2_INV: Matrix;
@@ -0,0 +1 @@
1
+ export * from './spectrum.js';