Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active April 27, 2023 06:04

Revisions

  1. mattdesl revised this gist Mar 19, 2023. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions about.md
    Original file line number Diff line number Diff line change
    @@ -41,6 +41,10 @@ It's unclear to me why the demo here does not require gamma-to-linear conversion

    https://github.com/geometrian/simple-spectral/tree/master/data

    ### Grayscale

    There is currently a bug where black-white interpolation will produce all-black output. I assume this is probably something that could be fixed but I haven't bothered yet.

    ### License

    I believe it may eventually be possible to license this as MIT *but* it does use the data tables from the paper's demo (not the MIT github code) which has no license associated. At the moment I am hesitant to mark it as MIT license because of that.
  2. mattdesl revised this gist Mar 19, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion about.md
    Original file line number Diff line number Diff line change
    @@ -50,7 +50,7 @@ However, if some JavaScript code is used to convert the CIE data from the GitHub

    ### Credits

    - Thanks to [@OneDayOfCrypto](https://twitter.com/OneDayOfCrypto) for his help answering questions on Twitter.
    - Thanks to [@OneDayOfCrypto](https://twitter.com/OneDayOfCrypto) for his help answering questions on Twitter - I went down this rabbit hole after seeing [their own implementation](https://twitter.com/OneDayOfCrypto/status/1636091072790003717) and learning about it
    - Most of this code is adapted from the paper, demo, and source of [Spectral Primary Decomposition for Rendering with sRGB Reflectance](https://graphics.geometrian.com/research/spectral-primaries.html) by Ian Mallett and Cem Yuksel
    - Also see Lars Wander's [recent repo](https://github.com/lwander/open-km) on dummy K-S coefficients for Kubelka-Munk mixing
    - Thanks to Scott Burns' website for related resources: http://scottburns.us/subtractive-color-mixture/
  3. mattdesl revised this gist Mar 19, 2023. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion about.md
    Original file line number Diff line number Diff line change
    @@ -54,4 +54,5 @@ However, if some JavaScript code is used to convert the CIE data from the GitHub
    - Most of this code is adapted from the paper, demo, and source of [Spectral Primary Decomposition for Rendering with sRGB Reflectance](https://graphics.geometrian.com/research/spectral-primaries.html) by Ian Mallett and Cem Yuksel
    - Also see Lars Wander's [recent repo](https://github.com/lwander/open-km) on dummy K-S coefficients for Kubelka-Munk mixing
    - Thanks to Scott Burns' website for related resources: http://scottburns.us/subtractive-color-mixture/
    - Also see Jose's sketchbook for additional resources: http://www.joesfer.com/?p=244
    - Also see Jose's sketchbook for additional resources: http://www.joesfer.com/?p=244
    - And thanks to ChatGPT 4, who was used as an educational companion to untangle some of the complexities and jargon in academic writing around Kubelka-Munk theory.
  4. mattdesl revised this gist Mar 19, 2023. 1 changed file with 5 additions and 1 deletion.
    6 changes: 5 additions & 1 deletion about.md
    Original file line number Diff line number Diff line change
    @@ -50,4 +50,8 @@ However, if some JavaScript code is used to convert the CIE data from the GitHub

    ### Credits

    Thanks to onedayofcrypto for his help on twitter!
    - Thanks to [@OneDayOfCrypto](https://twitter.com/OneDayOfCrypto) for his help answering questions on Twitter.
    - Most of this code is adapted from the paper, demo, and source of [Spectral Primary Decomposition for Rendering with sRGB Reflectance](https://graphics.geometrian.com/research/spectral-primaries.html) by Ian Mallett and Cem Yuksel
    - Also see Lars Wander's [recent repo](https://github.com/lwander/open-km) on dummy K-S coefficients for Kubelka-Munk mixing
    - Thanks to Scott Burns' website for related resources: http://scottburns.us/subtractive-color-mixture/
    - Also see Jose's sketchbook for additional resources: http://www.joesfer.com/?p=244
  5. mattdesl revised this gist Mar 19, 2023. 1 changed file with 6 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion about.md
    Original file line number Diff line number Diff line change
    @@ -45,4 +45,9 @@ https://github.com/geometrian/simple-spectral/tree/master/data

    I believe it may eventually be possible to license this as MIT *but* it does use the data tables from the paper's demo (not the MIT github code) which has no license associated. At the moment I am hesitant to mark it as MIT license because of that.

    However, if some JavaScript code is used to convert the CIE data from the GitHub into the required arrays for this to work, the entire script can be licensed as MIT without issue.
    However, if some JavaScript code is used to convert the CIE data from the GitHub into the required arrays for this to work, the entire script can be licensed as MIT without issue.


    ### Credits

    Thanks to onedayofcrypto for his help on twitter!
  6. mattdesl revised this gist Mar 19, 2023. 1 changed file with 21 additions and 0 deletions.
    21 changes: 21 additions & 0 deletions about.md
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,27 @@ https://github.com/geometrian/simple-spectral

    This converts sRGB triplet into spectral reflectance, then combines two curves with weighted geometric mean to produce a "mixed" colour.

    ### Installation

    Copy the code into a folder e.g. `mixer`. Install `canvas-sketch-cli` like so:

    ```sh
    # move into the new folder containing weighted_mean.js
    cd mixer/

    # install
    npm init -y
    npm i canvas-sketch-cli --save-dev
    ```

    Now you can run it:

    ```
    npx canvas-sketch weighted_mean.js --open
    ```

    ### Results

    Results, compared with interpolating `[R,G,B]` values directly:

    ![2023 03 19-15 16 44](https://user-images.githubusercontent.com/1383811/226186054-262cde81-7cfe-4173-9764-3b1af621b20d.png)
  7. mattdesl created this gist Mar 19, 2023.
    27 changes: 27 additions & 0 deletions about.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,27 @@
    Using data and code from:
    https://graphics.geometrian.com/research/spectral-primaries.html

    See their reference implementation for details (MIT license):
    https://github.com/geometrian/simple-spectral

    This converts sRGB triplet into spectral reflectance, then combines two curves with weighted geometric mean to produce a "mixed" colour.

    Results, compared with interpolating `[R,G,B]` values directly:

    ![2023 03 19-15 16 44](https://user-images.githubusercontent.com/1383811/226186054-262cde81-7cfe-4173-9764-3b1af621b20d.png)

    It does not always work. Here is a failure case compared to mixbox:

    ![2023 03 19-15 21 23](https://user-images.githubusercontent.com/1383811/226186105-c7f523f5-c071-4d70-a79f-be343a21d377.png)

    ### Gamma?

    It's unclear to me why the demo here does not require gamma-to-linear conversion before doing the mix, and linear-to-gamma correction after. I assume that the data tables may have some sort of gamma treatment. It seems like it would be better to do all the interpolation and averaging in linear space, but I would need to generate new data from the original CIE files here:

    https://github.com/geometrian/simple-spectral/tree/master/data

    ### License

    I believe it may eventually be possible to license this as MIT *but* it does use the data tables from the paper's demo (not the MIT github code) which has no license associated. At the moment I am hesitant to mark it as MIT license because of that.

    However, if some JavaScript code is used to convert the CIE data from the GitHub into the required arrays for this to work, the entire script can be licensed as MIT without issue.
    243 changes: 243 additions & 0 deletions weighted_mean.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,243 @@
    const canvasSketch = require("canvas-sketch");
    const Color = require("canvas-sketch-util/color");
    const Random = require("canvas-sketch-util/random");
    const math = require("canvas-sketch-util/math");
    const mixbox = require("mixbox");

    const settings = {
    dimensions: [2048, 2048],
    };

    // Data tables from:
    // https://graphics.geometrian.com/research/spectral-primaries.html
    // https://github.com/geometrian/simple-spectral

    // prettier-ignore
    const x_bar = [0.001368, 0.002236, 0.004243, 0.007650, 0.014310, 0.023190, 0.043510, 0.077630, 0.134380, 0.214770, 0.283900, 0.328500, 0.348280, 0.348060, 0.336200, 0.318700, 0.290800, 0.251100, 0.195360, 0.142100, 0.095640, 0.057950, 0.032010, 0.014700, 0.004900, 0.002400, 0.009300, 0.029100, 0.063270, 0.109600, 0.165500, 0.225750, 0.290400, 0.359700, 0.433450, 0.512050, 0.594500, 0.678400, 0.762100, 0.842500, 0.916300, 0.978600, 1.026300, 1.056700, 1.062200, 1.045600, 1.002600, 0.938400, 0.854450, 0.751400, 0.642400, 0.541900, 0.447900, 0.360800, 0.283500, 0.218700, 0.164900, 0.121200, 0.087400, 0.063600, 0.046770, 0.032900, 0.022700, 0.015840, 0.011359, 0.008111, 0.005790, 0.004109, 0.002899, 0.002049, 0.001440, 0.001000, 0.000690, 0.000476, 0.000332, 0.000235, 0.000166, 0.000117, 0.000083, 0.000059, 0.000042];
    // prettier-ignore
    const y_bar = [0.000039, 0.000064, 0.000120, 0.000217, 0.000396, 0.000640, 0.001210, 0.002180, 0.004000, 0.007300, 0.011600, 0.016840, 0.023000, 0.029800, 0.038000, 0.048000, 0.060000, 0.073900, 0.090980, 0.112600, 0.139020, 0.169300, 0.208020, 0.258600, 0.323000, 0.407300, 0.503000, 0.608200, 0.710000, 0.793200, 0.862000, 0.914850, 0.954000, 0.980300, 0.994950, 1.000000, 0.995000, 0.978600, 0.952000, 0.915400, 0.870000, 0.816300, 0.757000, 0.694900, 0.631000, 0.566800, 0.503000, 0.441200, 0.381000, 0.321000, 0.265000, 0.217000, 0.175000, 0.138200, 0.107000, 0.081600, 0.061000, 0.044580, 0.032000, 0.023200, 0.017000, 0.011920, 0.008210, 0.005723, 0.004102, 0.002929, 0.002091, 0.001484, 0.001047, 0.000740, 0.000520, 0.000361, 0.000249, 0.000172, 0.000120, 0.000085, 0.000060, 0.000042, 0.000030, 0.000021, 0.000015];
    // prettier-ignore
    const z_bar = [0.006450, 0.010550, 0.020050, 0.036210, 0.067850, 0.110200, 0.207400, 0.371300, 0.645600, 1.039050, 1.385600, 1.622960, 1.747060, 1.782600, 1.772110, 1.744100, 1.669200, 1.528100, 1.287640, 1.041900, 0.812950, 0.616200, 0.465180, 0.353300, 0.272000, 0.212300, 0.158200, 0.111700, 0.078250, 0.057250, 0.042160, 0.029840, 0.020300, 0.013400, 0.008750, 0.005750, 0.003900, 0.002750, 0.002100, 0.001800, 0.001650, 0.001400, 0.001100, 0.001000, 0.000800, 0.000600, 0.000340, 0.000240, 0.000190, 0.000100, 0.000050, 0.000030, 0.000020, 0.000010, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000];

    // prettier-ignore
    const d65 = [49.975500, 52.311800, 54.648200, 68.701500, 82.754900, 87.120400, 91.486000, 92.458900, 93.431800, 90.057000, 86.682300, 95.773600, 104.865000, 110.936000, 117.008000, 117.410000, 117.812000, 116.336000, 114.861000, 115.392000, 115.923000, 112.367000, 108.811000, 109.082000, 109.354000, 108.578000, 107.802000, 106.296000, 104.790000, 106.239000, 107.689000, 106.047000, 104.405000, 104.225000, 104.046000, 102.023000, 100.000000, 98.167100, 96.334200, 96.061100, 95.788000, 92.236800, 88.685600, 89.345900, 90.006200, 89.802600, 89.599100, 88.648900, 87.698700, 85.493600, 83.288600, 83.493900, 83.699200, 81.863000, 80.026800, 80.120700, 80.214600, 81.246200, 82.277800, 80.281000, 78.284200, 74.002700, 69.721300, 70.665200, 71.609100, 72.979000, 74.349000, 67.976500, 61.604000, 65.744800, 69.885600, 72.486300, 75.087000, 69.339800, 63.592700, 55.005400, 46.418200, 56.611800, 66.805400, 65.094100, 63.382800];
    // prettier-ignore
    const cieE = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ];
    // prettier-ignore
    const y_d65 = 2113.454951;
    // prettier-ignore
    const y_cieE= 21.3713;

    // prettier-ignore
    const s_r1 = [0.327457414, 0.323750578, 0.313439461, 0.288879383, 0.239205681, 0.189702037, 0.121746068, 0.074578271, 0.044433159, 0.028928632, 0.022316653, 0.016911307, 0.014181107, 0.013053143, 0.011986164, 0.011288715, 0.010906066, 0.010400713, 0.010637360, 0.010907663, 0.011032712, 0.011310657, 0.011154642, 0.010148770, 0.008918582, 0.007685576, 0.006705708, 0.005995806, 0.005537257, 0.005193784, 0.005025362, 0.005136363, 0.005433200, 0.005819986, 0.006400573, 0.007449529, 0.008583636, 0.010395762, 0.013565434, 0.019384516, 0.032084071, 0.074356038, 0.624393724, 0.918310033, 0.949253030, 0.958187833, 0.958187751, 0.958187625, 0.955679061, 0.958006155, 0.954101573, 0.947607606, 0.938681328, 0.924466683, 0.904606025, 0.880412199, 0.847787873, 0.805779127, 0.752531854, 0.686439397, 0.618694571, 0.540264444, 0.472964416, 0.432701597, 0.405358046, 0.385491835, 0.370983585, 0.357608702, 0.348712800, 0.344880119, 0.341917877, 0.339531093, 0.337169504, 0.336172019, 0.335167443, 0.334421625, 0.334008760, 0.333915793, 0.333818455, 0.333672775, 0.333569513];
    // prettier-ignore
    const s_g1 = [0.331861713, 0.329688188, 0.327860022, 0.319173580, 0.294322584, 0.258697065, 0.188894319, 0.125388382, 0.078687060, 0.053143271, 0.042288146, 0.033318346, 0.029755948, 0.030331251, 0.030988572, 0.031686355, 0.034669962, 0.034551957, 0.040684806, 0.054460037, 0.080905287, 0.146348303, 0.379679643, 0.766744269, 0.876214748, 0.918491656, 0.940655563, 0.953731885, 0.961643280, 0.967200020, 0.970989746, 0.972852304, 0.973116594, 0.973351069, 0.973351116, 0.972261080, 0.973351022, 0.973148495, 0.971061306, 0.966371306, 0.954941968, 0.913578990, 0.364348804, 0.071507243, 0.041230434, 0.032423874, 0.031924630, 0.031276033, 0.032630370, 0.029530872, 0.031561761, 0.035674218, 0.041403005, 0.050604260, 0.063434300, 0.078918245, 0.099542743, 0.125595760, 0.157590910, 0.195398239, 0.231474475, 0.268852136, 0.296029164, 0.309754994, 0.317815883, 0.322990347, 0.326353848, 0.329143902, 0.330808727, 0.331482690, 0.331984550, 0.332341173, 0.332912009, 0.332919280, 0.333027673, 0.333179705, 0.333247031, 0.333259349, 0.333275050, 0.333294328, 0.333309425];
    // prettier-ignore
    const s_b1 = [0.340680792, 0.346561187, 0.358700493, 0.391947027, 0.466471731, 0.551600896, 0.689359611, 0.800033347, 0.876879781, 0.917928097, 0.935395201, 0.949770347, 0.956062945, 0.956615607, 0.957025265, 0.957024931, 0.954423973, 0.955047329, 0.948677833, 0.934632300, 0.908062000, 0.842341039, 0.609165715, 0.223106961, 0.114866670, 0.073822768, 0.052638729, 0.040272309, 0.032819463, 0.027606196, 0.023984891, 0.022011333, 0.021450205, 0.020828945, 0.020248311, 0.020289391, 0.018065342, 0.016455742, 0.015373260, 0.014244178, 0.012973962, 0.012064974, 0.011257478, 0.010182725, 0.009516535, 0.009388293, 0.009887619, 0.010536342, 0.011690569, 0.012462973, 0.014336665, 0.016718175, 0.019915666, 0.024929056, 0.031959674, 0.040669554, 0.052669382, 0.068625111, 0.089877232, 0.118162359, 0.149830947, 0.190883409, 0.231006403, 0.257543385, 0.276826039, 0.291517773, 0.302662506, 0.313247301, 0.320478325, 0.323636995, 0.326097309, 0.328127369, 0.329917976, 0.330907901, 0.331803633, 0.332396627, 0.332740781, 0.332820857, 0.332901731, 0.333025967, 0.333111083];

    // prettier-ignore
    const s_r2 = [0.328455134, 0.324039100, 0.315349590, 0.292792770, 0.246316933, 0.198108029, 0.130068113, 0.079657502, 0.047536766, 0.030925762, 0.023739245, 0.017858899, 0.014560638, 0.012790919, 0.011391265, 0.010621609, 0.010019665, 0.009843010, 0.010040448, 0.010026949, 0.009896261, 0.010490400, 0.009780279, 0.008394966, 0.007078490, 0.006339279, 0.005491672, 0.004880634, 0.004483955, 0.004185756, 0.004029708, 0.004096559, 0.004260582, 0.004472863, 0.004811227, 0.005409461, 0.006287819, 0.007615900, 0.009731549, 0.013081085, 0.019375748, 0.327707567, 0.538874667, 0.725699391, 0.951408718, 0.962637428, 0.966971579, 0.968007753, 0.967112589, 0.963775324, 0.958418605, 0.952601048, 0.944569777, 0.931722624, 0.913700042, 0.891598969, 0.860505987, 0.824225062, 0.773069484, 0.709647070, 0.642591809, 0.562173723, 0.490358422, 0.445050267, 0.414903338, 0.392213639, 0.375678397, 0.360661444, 0.349760473, 0.347712371, 0.339279547, 0.342893608, 0.338489141, 0.336913338, 0.341734672, 0.334898485, 0.334735839, 0.334317322, 0.333924136, 0.333359358, 0.333012936];
    // prettier-ignore
    const s_g2 = [0.330648932, 0.329150364, 0.326106934, 0.314306096, 0.285621445, 0.246483644, 0.176356833, 0.115924753, 0.072260822, 0.048849075, 0.038884781, 0.030676675, 0.026470494, 0.024892729, 0.024141373, 0.024877463, 0.026478710, 0.030160991, 0.038137818, 0.051247767, 0.089042464, 0.316275641, 0.447692245, 0.645266804, 0.827179315, 0.921339153, 0.942909702, 0.955503903, 0.963102866, 0.968404133, 0.972079059, 0.973896551, 0.975085761, 0.976143135, 0.976820260, 0.976783210, 0.976380637, 0.975499356, 0.973843427, 0.971305193, 0.966234176, 0.658916933, 0.449088083, 0.263712302, 0.038957672, 0.028189759, 0.023919124, 0.022538392, 0.022706962, 0.024634248, 0.027942452, 0.031543891, 0.036587085, 0.044729441, 0.056138794, 0.070030169, 0.089641578, 0.111148860, 0.141853788, 0.178303772, 0.212975249, 0.253865556, 0.283887105, 0.301548721, 0.311745496, 0.319037251, 0.323070178, 0.326947909, 0.329858486, 0.330452606, 0.330682210, 0.335519233, 0.330196327, 0.333260022, 0.331231570, 0.334947675, 0.331484024, 0.331632713, 0.331732129, 0.331951176, 0.332076988];
    // prettier-ignore
    const s_b2 = [0.340895932, 0.346810535, 0.358543476, 0.392901134, 0.468061621, 0.555408327, 0.693575054, 0.804417744, 0.880202412, 0.920225163, 0.937375974, 0.951464426, 0.958968868, 0.962316352, 0.964467362, 0.964500928, 0.963501626, 0.959995999, 0.951821734, 0.938725283, 0.901061275, 0.673233959, 0.542527476, 0.346338230, 0.165742195, 0.072321568, 0.051598626, 0.039615464, 0.032413179, 0.027410111, 0.023891233, 0.022006890, 0.020653657, 0.019384003, 0.018368514, 0.017807329, 0.017331544, 0.016884744, 0.016425024, 0.015613722, 0.014390077, 0.013375499, 0.012037251, 0.010588306, 0.009633610, 0.009172813, 0.009109297, 0.009453855, 0.010180450, 0.011590428, 0.013638943, 0.015855062, 0.018843138, 0.023547934, 0.030161164, 0.038370863, 0.049852435, 0.064626077, 0.085076729, 0.112049158, 0.144432942, 0.183960721, 0.225754473, 0.253401011, 0.273351165, 0.288749110, 0.301251423, 0.312390644, 0.320381036, 0.321835018, 0.330038236, 0.321587149, 0.331314518, 0.329826618, 0.327033724, 0.330153783, 0.333780041, 0.334049853, 0.334343601, 0.334689271, 0.334909795];

    // prettier-ignore
    const s_r3 = [0.325499558, 0.320381406, 0.308469037, 0.288245363, 0.250900198, 0.208049512, 0.142822037, 0.090291304, 0.055086555, 0.034487258, 0.025360654, 0.023786500, 0.018435917, 0.020584585, 0.016358498, 0.015433283, 0.013406507, 0.012352481, 0.011964661, 0.011659590, 0.011229869, 0.010655116, 0.009814057, 0.008714740, 0.007540184, 0.006377456, 0.005484956, 0.004817804, 0.004382595, 0.004172254, 0.004101861, 0.004138331, 0.004289889, 0.004619630, 0.004978489, 0.006264624, 0.006880366, 0.008266738, 0.011195008, 0.016993999, 0.031973010, 0.115735649, 0.912667746, 0.960557237, 0.971874841, 0.975183425, 0.976383571, 0.976016150, 0.976332613, 0.975524178, 0.973447797, 0.969445120, 0.963951581, 0.956231695, 0.945406940, 0.930681934, 0.910253756, 0.881563264, 0.842731316, 0.795244879, 0.740974931, 0.671260447, 0.595695790, 0.528500631, 0.476964847, 0.436575888, 0.406956401, 0.385054719, 0.369656118, 0.358701127, 0.351247023, 0.345647046, 0.342009756, 0.339184203, 0.337536748, 0.336140636, 0.335294339, 0.334707151, 0.334286544, 0.333995833, 0.333780605];
    // prettier-ignore
    const s_g3 = [0.328971397, 0.326105757, 0.319138530, 0.306444351, 0.279644852, 0.244314339, 0.180844467, 0.122661839, 0.077640735, 0.050761397, 0.039106761, 0.039570798, 0.032249338, 0.041747124, 0.035255927, 0.042991185, 0.039081117, 0.040859842, 0.048631057, 0.064256482, 0.092862239, 0.152844904, 0.333779721, 0.715669114, 0.853694727, 0.906496602, 0.933739871, 0.949983277, 0.959843022, 0.965805752, 0.969868483, 0.972791835, 0.974563464, 0.974732329, 0.976920722, 0.971668010, 0.976041654, 0.977343678, 0.975682218, 0.971328847, 0.957863449, 0.875520427, 0.079735041, 0.032657233, 0.021814725, 0.018591957, 0.017272418, 0.017277290, 0.016651778, 0.016751603, 0.017912024, 0.020450903, 0.023970674, 0.028941852, 0.035923777, 0.045389004, 0.058440618, 0.076605271, 0.100824530, 0.129811406, 0.161818833, 0.200765154, 0.239583667, 0.270729160, 0.291638057, 0.306316960, 0.315602301, 0.321740892, 0.325694404, 0.328204944, 0.329848517, 0.330955419, 0.331696707, 0.332202377, 0.332539111, 0.332762621, 0.332913970, 0.333015306, 0.333091990, 0.333139454, 0.333177806];
    // prettier-ignore
    const s_b3 = [0.345528956, 0.353512783, 0.372392404, 0.405310270, 0.469454942, 0.547636144, 0.676333494, 0.787046856, 0.867272709, 0.914751345, 0.935532585, 0.936642703, 0.949314745, 0.937668291, 0.948385575, 0.941575532, 0.947512376, 0.946787678, 0.939404282, 0.924083928, 0.895907892, 0.836499980, 0.656406221, 0.275616145, 0.138765088, 0.087125941, 0.060775172, 0.045198918, 0.035774382, 0.030021993, 0.026029656, 0.023069834, 0.021146647, 0.020648041, 0.018100789, 0.022067367, 0.017077980, 0.014389585, 0.013122774, 0.011677154, 0.010163542, 0.008743924, 0.007597213, 0.006785530, 0.006310435, 0.006224618, 0.006344011, 0.006706561, 0.007015609, 0.007724220, 0.008640180, 0.010103977, 0.012077744, 0.014826453, 0.018669283, 0.023929061, 0.031305625, 0.041831463, 0.056444151, 0.074943712, 0.097206230, 0.127974392, 0.164720533, 0.200770194, 0.231397076, 0.257107123, 0.277441256, 0.293204331, 0.304649395, 0.313093812, 0.318904293, 0.323397294, 0.326293188, 0.328612914, 0.329923415, 0.331095719, 0.331790241, 0.332275484, 0.332618565, 0.332860626, 0.333035850];

    var illum = d65;
    var y_illum = y_d65;
    var s_r = s_r1;
    var s_g = s_g1;
    var s_b = s_b1;

    // prettier-ignore
    const pairs = [
    [[0, 33, 133 ], [252, 211, 0]],
    [[0, 33, 133 ], [255, 105, 0]],
    [[25, 0, 89], [252, 211, 0]],
    [[123, 72, 0 ],[107, 148, 4]],
    [[255,255,255], [0,0,0]],
    [[255,255,255], [0,255,0]],
    [[255,255,255], [255,0,0]],
    [[0,255,255], [0,0,255]],
    ]

    const sketch = () => {
    // random pairs
    const [colorA, colorB] = Random.pick(pairs);

    // fixed inputs
    // const [colorA, colorB] = [
    // [0, 33, 133],
    // [252, 211, 0],
    // ];

    return ({ context, width, height }) => {
    context.fillStyle = Color.parse(colorA).hex;
    context.fillRect(0, 0, width / 2, height);

    const bHeight = height / 3;

    context.textAlign = "left";
    context.textBaseline = "middle";
    const fontSize = bHeight / 16;
    context.font = `${fontSize}px monospace`;
    ramp(context, colorA, colorB, width, bHeight, (a, b, t) => {
    return math.lerpArray(a, b, t);
    });
    context.fillStyle = "white";
    context.fillText(
    "sRGB interpolation",
    fontSize * 2,
    bHeight / 2 - fontSize / 2
    );

    context.translate(0, bHeight);
    ramp(context, colorA, colorB, width, bHeight, (a, b, t) => {
    const specA = sRGBToSpectrum(colorA);
    const specB = sRGBToSpectrum(colorB);
    return spectrumToSRGB(mixSpectrums(specA, specB, t));
    });
    context.fillStyle = "white";
    context.fillText(
    "spectrum interpolation (weighted geometric mean)",
    fontSize * 2,
    bHeight / 2 - fontSize / 2
    );

    context.translate(0, bHeight);
    ramp(context, colorA, colorB, width, bHeight, (a, b, t) => {
    return mixbox.lerp(a, b, t);
    });
    context.fillStyle = "white";
    context.fillText(
    "mixbox interpolation (Kubelka-Munk)",
    fontSize * 2,
    bHeight / 2 - fontSize / 2
    );
    };

    function ramp(context, a, b, width, height, mixer) {
    for (let i = 0; i < width; i++) {
    const t = i / (width - 1);
    const rgb = mixer(a, b, t);
    context.fillStyle = Color.parse(rgb).hex;
    context.fillRect(i, 0, 1, height);
    }
    }

    function mixSpectrums(specA, specB, t) {
    if (specA.length !== specB.length)
    throw new Error("spectrum length mismatch");
    const mixedSpectrum = new Array(specA.length).fill(0);

    for (let i = 0; i < specA.length; i++) {
    const a = specA[i];
    const b = specB[i];

    const t0 = t;
    const w1 = t0;
    const w0 = 1 - t0;
    const X = Math.pow(a, w0) * Math.pow(b, w1);
    mixedSpectrum[i] = X;
    }
    return mixedSpectrum;
    }

    function sRGBToSpectrum(sRGB) {
    // NOTE: There is no gamma-to-linear conversion here
    // It produces odd results when I apply it. I wonder
    // if the data tables has somehow already been treated to account for gamma?
    const [r, g, b] = sRGB.map((x) => x / 0xff);
    const spec = new Array(s_r.length);
    // in 0..1 range
    for (var i = 0; i < s_r.length; ++i) {
    spec[i] = s_r[i] * r + s_g[i] * g + s_b[i] * b;
    }
    return spec;
    }

    function spectrumToSRGB(s) {
    var x = 0,
    y = 0,
    z = 0;
    for (var i = 0; i < illum.length; ++i) {
    var intens = s[i] * illum[i];
    x += x_bar[i] * intens;
    y += y_bar[i] * intens;
    z += z_bar[i] * intens;
    }

    // The transformation matrix is computed using the algorithm outlined on this page:
    // http://brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
    var r =
    (3.2408302291321256 * x -
    1.5373169035626748 * y -
    0.4985892660203271 * z) /
    y_illum;
    var g =
    (-0.9692293208748544 * x +
    1.8759397940918867 * y +
    0.04155444365280374 * z) /
    y_illum;
    var b =
    (0.05564528732689767 * x -
    0.20403272019862467 * y +
    1.0572604592110555 * z) /
    y_illum;

    // NOTE: There is no linear-to-gamma conversion here
    // It produces odd results when I apply it. I wonder
    // if the data tables has somehow already been treated to account for gamma?
    return [r, g, b].map((x) => Math.round(x * 0xff));
    }
    };

    canvasSketch(sketch, settings);

    ////// Utils
    // These are not used above but may be useful
    // within the scope of Kubelka-Munk color mixing algorithms

    function layer(R1, T1, R2, T2) {
    const R = R1 + (T1 * T1 * R2) / (1 - R1 * R2);
    const T = (T1 * T2) / (1 - R1 * R2);
    return {
    R,
    T,
    };
    }

    function reflectance_mix(ks) {
    return 1.0 + ks - Math.sqrt(ks * ks + 2.0 * ks);
    }

    function saunderson_mix(ks) {
    const K1 = 0.0031;
    const K2 = 0.65;
    const R = reflectance_mix(ks);
    return ((1.0 - K1) * (1.0 - K2) * R) / (1.0 - K2 * R);
    }

    function R_to_KS(R) {
    return Math.pow(1 - R, 2) / (2 * R);
    }

    function computeRT(K = 0.5, S = 0.5, h = 0.5) {
    const K_S = K / S;
    const a = 1 + K_S;
    const b = Math.sqrt(a * a - 1);
    const bsh = b * S * h;
    const c = a * Math.sinh(bsh) + b * Math.cosh(bsh);
    const R = Math.sinh(bsh / c);
    const T = b / c;
    return {
    R,
    T,
    };
    }