Light spectrum to RGB tool

by PhilQ

HTML

<div id="container">
  <label>Color method:</label>
  <div id="rainbows"></div>
  <label for="wavelength">Wavelength:</label>
  <input type="number" id="wavelength" />
  <input type="range" id="wavelength-slider" />
</div>

<div id="card-wrapper">
  <div class="card">
    <div></div>
    <div></div>
  </div>
</div>


<svg id="pattern" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 2560 1600" enable-background="new 0 0 2560 1600" xml:space="preserve">
  <pattern width="200" height="200" patternUnits="userSpaceOnUse" id="New_Pattern" viewBox="0 -200 200 200" overflow="visible">
    <g>
      <rect y="-200" fill="#ffffff" width="200" height="200"/>
      <path d="M103.371-82.318c10.899,7.046,22.825,14.239,35.833,21.522c-7.283-13.008-14.476-24.933-21.522-35.833    c-2.724,1.951-5.528,3.917-8.414,5.897C107.288-87.846,105.322-85.041,103.371-82.318z"/>
      <path d="M82.318-103.371c-1.576,1.129-3.126,2.253-4.648,3.371c1.522,1.118,3.072,2.242,4.648,3.371    c0.734-1.135,1.466-2.258,2.196-3.371C83.784-101.113,83.052-102.236,82.318-103.371z"/>
      <path d="M82.318-96.629C75.272-85.73,68.079-73.804,60.796-60.796c13.008-7.283,24.933-14.476,35.833-21.522    c-1.951-2.724-3.917-5.528-5.897-8.414C87.846-92.712,85.041-94.678,82.318-96.629z"/>
      <path d="M96.629-82.318c1.129,1.576,2.253,3.126,3.371,4.648c1.118-1.522,2.242-3.072,3.371-4.648    c-1.135-0.734-2.258-1.466-3.371-2.196C98.887-83.784,97.764-83.052,96.629-82.318z"/>
      <path d="M117.682-103.371c7.046-10.899,14.239-22.825,21.522-35.833c-13.008,7.283-24.933,14.476-35.833,21.522    c1.951,2.724,3.917,5.528,5.897,8.414C112.154-107.288,114.958-105.322,117.682-103.371z"/>
      <path d="M100-115.486c-3.179,2.087-6.268,4.16-9.268,6.218c-2.058,3-4.131,6.089-6.218,9.268c2.087,3.179,4.16,6.268,6.218,9.268    c3,2.058,6.089,4.131,9.268,6.218c3.179-2.087,6.268-4.16,9.268-6.218c2.058-3,4.131-6.089,6.218-9.268   ...

SCSS

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#container {
  display: flex;
  width: 1000px;
  margin: 0 auto;
  padding: 20px;
  flex-direction: column;
  justify-content: center;
  gap: 1rem;
  background: #fff;
  box-shadow:
    0 10px 25px -5px rgba(0, 0, 0, 0.5),
    0 5px 10px -5px rgba(0, 0, 0, 0.5);
  border-radius: 6px;
  font-size: 25px;
}

label {
  text-transform: uppercase;
  font-size: 0.8rem;
  color: rgba(0, 0, 0, 0.3);
  margin-bottom: -0.5rem;
}
select, input {
  font-size: 1.4rem;
  width: 100%;

  &[type="radio"] {
    width: auto;
    margin-right: 0.5rem;
  }
}

.rainbow {
  width: 100%;
  height: 2rem;
  background: #eee;
}

@property --mx {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --my {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

#pattern, #pattern2 {
  position: absolute;
  z-index: -1;
  opacity: 0;
  width: 0;
  height: 0;
}
#pattern2 path {
  fill: #fff;
}

#card-wrapper {
  position: relative;
  width: 100%;
  height: 500px;
  perspective: 1500px;
  perspective-origin: 50% 50%;

  &:not(:hover) .card {
    animation-name: card-neutral;
    animation-duration: 0.35s;
    animation-timing-function: ease;
    animation-fill-mode: forwards;
    animation-iteration-count: 1;
  }
}

@keyframes card-neutral {
  100% {
    --mx: 0.5;
    --my: 0.5;
  }
}

.card {
  --mx: 0;
  --my: 0;
  position: absolute;
  left: 50%;
  top: 0;
  translate: -50% 0;
  width: 1000px;
  height: 400px;
  border-radius: 1rem;
  overflow: hidden;
  box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 1);
  background: repeating-linear-gradient(-35deg, #ccc 0%, #eee 20%, #ccc 40%);
  margin: 3rem auto 0;
  transform:
    rotateX(calc((2 * var(--my) - 1) * -20deg))
    rotateY(calc((2 * var(--mx) - 1) *...

JavaScript

/**
 * (c) All rights reserved - PvH
 * 
 * Based on article at:
 * https://www.baeldung.com/cs/rgb-color-light-frequency
 * 
 * CIE 1931 XYZ color-matching multi-lobe, piecewise Gaussian fit:
 * https://jcgt.org/published/0002/02/01/paper.pdf
 */

const lambda_min = 380;
const lambda_max = 750;

let rainbows = null,
  inputNumber = null,
  inputRange = null,
  fakeChangeEvent = new Event("change")
  lambda = 405, // 0.5 * (lambda_min + lambda_max),
  lambdaToRgb = null,
  dummy = null;

const makeElement = (tag, props = {}, events = {}) => {
  let element = document.createElement(tag);
  for (const [k, v] of Object.entries(props)) {
    element.setAttribute(k, v);
  }
  for (const [k, v] of Object.entries(events)) {
    element.addEventListener(k, v);
  }
  return element;
};

const clamp = (v, min = 0, max = 1) => Math.min(max, Math.max(min, v));

const map = (v, from_min, from_max, to_min, to_max) => {
  return to_min + (to_max - to_min) * (v - from_min) / (from_max - from_min);
};

const rgbToHex = (rgb) => `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`;

const colorMappings = {
  glynn: [
    "Earl F. Glynn mapping",
    [380, 420, 440, 490, 510, 580, 645, 700, 780],
    (lambda) => {
      const r = lambda >= 580
        ? 1
        : (
          lambda >= 510
            ? ((lambda - 510) / (580 - 510))
            : (
              lambda >= 440
              ? 0
              : ((440 - lambda) / (440 - 380))
            )
        );

      const g = lambda >= 645 || lambda < 440
        ? 0
        : (
          lambda >= 580
            ? ((645 - lambda) / (645 - 580))
            : (
              lambda >= 490
              ? 1
              : ((lambda - 440) / (490 - 440))
            )
        );

      const b = lambda >= 510
        ? 0
        : (
          lambda >= 490
            ? ((510 - lambda) / (510 - 490))
            : 1
        );

      const...