HSL Picker

by Rolf

HTML

<div id="hsl-picker">
  <fieldset class="controls">
    <legend>HSL Farbnavigator</legend>
    <div class="view">
      <label for="s-view">Blickwinkel: </label>
      <span> 
        <input id="s-view" type="range" min="-45" max="135" value="25" step="1">
      </span>
      <output></output>
    </div>
    <div class="hue">
      <label for="s-hue">Farbton: </label>
      <span> 
        <input id="s-hue" type="range" min="0" max="360" value="0" step="1">
      </span>
      <output></output>
    </div>
    <div class="light">
      <label for="s-lightness">Helligkeit: </label>
      <span> 
        <input id="s-lightness" type="range" min="0" max="100" value="50" step="1" list="range_0_100">
      </span>
      <output></output>
      </div>
    <div class="saturation">
      <label for="s-saturation">Sättigung: </label>
      <span> 
        <input id="s-saturation" type="range" min="0" max="100" value="50" step="1" list="range_0_100">
      </span>
      <output></output>
    </div>
    <div class="mode">
      <label>Modus:</label>
      <span>
      <label><input type="radio" name="mode" value="move-disk" checked>Bewege Farbscheibe</label>
      <label><input type="radio" name="mode" value="move-bar">Bewege Helligkeitsbalken</label>
      </span>
    </div>
    <output id="rendertime"></output>
    <datalist id="range_0_100">
      <option value="50"></option>
    </datalist>
  </fieldset>
  <div class="hue-select">
    <canvas class="hue-ring" width="800" height="800"></canvas>
    <div class="light-bar">
      <!-- 3D-Modell für die Helligkeits-"Stange" -->
      <div class="cap"></div>
      <div class="bar"></div> 
      <div class="bar"></div> 
      <div class="bar"></div> 
      <div class="bar"></div> 
      <div class="bar"></div> 
      <div class="bar"></div> 
      <div class="bar"></div> 
    </div>
  </div>
</div>

CSS

#hsl-picker {
  --hue: 0deg;
  --light: 0;
  --view: 80deg;
  --hue-r1: 80cqw;
  --bar-width: 5cqw;
  --bar-height: 60cqw;
  --bar-safe: 1px;
  
  width: 800px;
  container-type: inline-size;

  .controls {
    padding-inline: 0.5em;
    position: relative;
    xz-index: 1;
    background-color: hsl(320deg 5% 95%);
    div {
      display: flex;
      align-items: center;
      margin: 0.5em 0;
    }
    > div > label {
      flex: 0 0 5.5em;
      text-align: right;
    }
    span {
      flex: 1 0 auto;
      padding: 0.2em 0.5em;
      
      input[type=range] {
        width: 100%;
        margin: 0.2em 0;
      }
      
      .saturation & {
        background: linear-gradient(to right,
                        hsl(var(--hue)   0% calc(1%*var(--light))),
                        hsl(var(--hue) 100% calc(1%*var(--light))))
                    1em top / calc(100% - 2em) 100% no-repeat;
      }
    }
    output {
      flex: 0 0 4em;
      background-color: hsl(60deg 5% 85%);;
      height: 1.5em;      
    }
  }
}

.hue-select {
  --extra-height: calc(0.15 * var(--bar-height));
  position: relative;
  width: 100%;
  height: calc(2*var(--extra-height) + var(--bar-height));
  transform-style: preserve-3d;
  transform: rotateX(calc(-1*var(--view)));
}
  
.hue-ring {
  --light-shift: 0px;
  position: absolute;
  left: calc((100% - var(--hue-r1)) / 2);
  width: var(--hue-r1);
  aspect-ratio: 1/1;
  transform-origin: center;
  transform: translateY(calc(var(--extra-height) - var(--hue-r1)/2 + var(--light-shift)))
             rotateX(90deg)
             rotateZ(-45deg);
}
.controls:has([value=move-disk]:checked) ~ .hue-select .hue-ring {
  --light-shift: calc(var(--bar-height) * (100 - var(--light))/100);
}

.controls:has([value=move-bar]:checked) ~ .hue-select .hue-ring {
  --light-shift: calc( var(--bar-height) / 2);
}

.controls:has([value=move-bar]:checked) ~ .hue-select .light-bar {
  transform:
    translateY(calc(var(--bar-height)*(var(--light) -...

JavaScript

const picker = document.querySelector("#hsl-picker");
const can1 = picker.querySelector(".hue-ring");
const can2 = picker.querySelector(".light-bar .cap");
// showHueDisk(ctx2D, 50);

can1.setAttribute("width" , can1.clientWidth );
can1.setAttribute("height", can1.clientHeight);
//can2.setAttribute("width" , can2.clientWidth );
//can2.setAttribute("height", can2.clientHeight);
pickStyle = window.getComputedStyle(picker);

function paintLightBar(ctx2D) {
   const view = CSSUnitValue.parse(pickStyle.getPropertyValue("--view"));
   const viewAngle = view.to("rad").value,
         width = ctx2D.canvas.width,
         rx = width/2,
         ry = rx * Math.sin(viewAngle),
         height = ctx2D.canvas.height - 2*(ry+2);
   
   console.log("bar height is", height, "view is",view,"view Angle is",viewAngle,"rad, rx is", rx, ", ry=", ry);
   const step = 10;
   const dy = height*step/100;
   for (let i=step; i<=100; i+=step) {
      // y2: Ohne +1 ist das unterste Segment nicht hoch genug
      const y1 = ry+1+ height*(100-i)/100,
            y2 =  y1 + dy + 2;
      ctx2D.fillStyle = `hsl(0 0% ${i}%)`;
      ctx2D.beginPath();
      ctx2D.moveTo(width/2 - rx, y1);
      ctx2D.lineTo(width/2 - rx, y2);
      // Mittlerer Stützpunkt: Ohne +1 für y ist die Kurve zu flach
      ctx2D.quadraticCurveTo(width/2 - rx, y2 + ry, width/2   , y2+ry+1);
      ctx2D.quadraticCurveTo(width/2 + rx, y2 + ry, width/2+rx, y2);

      ctx2D.lineTo(width/2 + rx, y1);
      ctx2D.closePath();
      //ctx2D.ellipse(width/2 - rx, ry+1+height*(100-i)/100, rx, ry, 0, 0, Math.PI*2);
      ctx2D.fill();
   }
   ctx2D.beginPath();
   ctx2D.fillStyle = "white";
   ctx2D.strokeStyle = "black";
   ctx2D.lineWidth = 0.5;
   ctx2D.ellipse(width/2, ry+1, rx, ry, 0, 0, Math.PI*2);
   ctx2D.fill();
   ctx2D.stroke();
   ctx2D.beginPath();
   ctx2D.moveTo(0, ry+1);
   ctx2D.lineTo(0, ry+1+height);
   ctx2D.moveTo(width, ry+1);
   ctx2D.lineTo(width, ry+1+height);
   ctx2D.stroke();

}
function...