Eine Farbwähler-Bastelanleitung

incl. HSB-, HSL-, RGB- und HEX-Umrechnung

Naturlich gibt es bereits mehrere Spezilllionen von Online-Farbwählern, z.B. hier, hier oder hier. Und auch entspr. jQuery-Plugins. Aber darum geht es ja gar nicht 🙂 sondern um die Bastelarbeit – und die Einsicht, dass man mit rudimentären Kenntnissen von HTML, CSS und JS lustige und praktische Web-Apps bauen kann.

1. Worum gehts?

Um einen webbasierten, interaktiven Farbwähler, der die Farbwerte in verschiedenen Farbräumen & -modellen ausgibt. Zuerst das Ergebnis:

HSB:
hue: 0°,
saturation: 0%,
brightness: 100%
HSL:
hue: 0°,
saturation: 100%,
brightness: 100%
RGB:
red: 0,
green: 0,
blue: 0
HEX: #ffffff

Einsatzmöglichkeiten sind z.B. UIs, in denen Benutzende Farben auswählen können – oder einfach ein kleines Nachschlage- und Übersetzungstool für Farbwerte.

Kleine Einschränkungen: Da die Farbwerte auf Basis von Koordinaten beweglicher UI-Elemente berechnet werden, kann es zu Rundungsfehlern und kleinen Abweichungen von anderen Farbwählern kommen. Es gibt bisher nur eine rudimentäre responsive Anpassung (von dieser Seite und dem Color Picker) an die Bildschirmgröße und es ist (noch) keine manuelle Eingabe von Farbwerten möglich. Aber für die v2 müssen ja auch noch Features über bleiben…

2. Die Bestandteile

Der Color Picker hat – genauso wie z.B. der von Photoshop oder anderen Umgebungen – zwei Interaktionsmöglichkeiten. Die eine ist das „große“ Farbfeld, in dem die Helligkeit und Sättigung durch Verschieben eines „Kringels“ eingestellt werden kann. Die andere ist ein Schieberegler für den Farbwert. In Kombination von Farbwert, Helligkeit und Sättigung ergibt sich die endgültige Farbe. Dem liegt das gängige HSV-Modell zur Farbbeschreibung zu Grunde.

Ohne hier zu tief in die Welt der Farbräume und -modelle abzutauchen, eine kurze Auffrischung: Farbwert-Sättigung-Hellwert-Farbräume (HSV für hue, saturation und value; mit entweder brightness – dann HSB – oder lightness – dann HSL – als Helligkeitswert) sind trotz komplexer Theorie recht einfach in der Handhabung und lassen sich im Webdesign via CSS als CSS-Wert hsl([hue],[saturation]%,[lightness]%) für Farb-Parameter nutzen, z.B. als background-color:hsl(349,59%,46%);.

Neben HSV-Modellen ist vor allem das subtraktive Farbmischmodell RGB (Mischung von red, green und blue) oder das additive Mischmodell wie CMYK (Mischung von cyan, magenta, yellow und schwarz bzw. key) verbreitet. Letzteres wird vor allem im Druck angewandt und ist für leuchtende Medien wie Bildschirme eher ungeeignet, wird daher hier vernachlässigt. Die RGB-Angabe ist allerdings in CSS als rgb([rotwert],[grünwert],[blauwert]) als Eigenschaft nutzbar – z.B. background-color:rgb(191,48,72);. Eine Variante der RGB-Codierung ist die Darstellung der drei zu mischenden Farbwerte in hexadezimaler Darstellung (HEX) als #RRGGBB, z.B. als background-color:#bf3048;. Alle drei Beispiele, background-color:hsl(349,59%,46%);, background-color:rgb(191,48,72); und background-color:#bf3048; beschreiben übrigens die gleiche Farbe, nämlich „medienmarmela.de-rot“ 🙂

Der Color Picker soll also die eingestellte Farbe in HSB, HSL, RGB und HEX ausgeben. Eine Eingabe numerischer Farbwerte und Rückübersetzung in Farbe und Position des Color Picker ist „geplant“.

2.1 Das Farbfeld für Helligkeit und Sättigung

Um Helligkeit und Sättigung einer Grundfarbe zu visualisieren, liegen im <div id="colorpicker"> drei verschachtelte <divs> übereinander: Eins mit der (über den Schieberegler einstellbaren, s.u.) Grundfarbe (#basecolor), ein weiteres mit einem Sättigungsverlauf (#saturation, mit einem linear-gradient von links, weiß, nach rechts, transparent) und ein drittes mit einem Helligkeitsverlauf (#brightness, ebenfalls mit einem linear-gradient von unten, schwarz, nach oben, transparent):

HTML

<div id="colorpicker">
  <div id="basecolor">
    <div id="saturation">
      <div id="brightness"></div>
    </div>
  </div>
</div>

Alle drei werden absolute übereinander positioniert und mit Grundfarbe bzw. den Verläufen bestückt:

CSS

#colorpicker {
    background:#333435;
    max-width:500px;
    min-height:300px;
    position:relative;
    margin:0 auto;
    display: flex;
    flex-flow: row wrap;
    width: calc(100% - 20px);
    padding:10px;
    min-width: 330px;
}
#basecolor {
    background:#ff0000;
    position:relative;
    width:300px;
    height:300px;
    top:0;
    left:0;
}
#saturation {
    position:absolute;
    width:100%;
    height:100%;
    top:0;
    left:0;
    background-image:linear-gradient(to right,white,rgba(255,255,255,0));
}
#brightness {
    position:absolute;
    width:100%;
    height:100%;
    top:0;
    left:0;
    background-image:linear-gradient(to top,black,rgba(255,255,255,0));
}

Das Ergebnis sieht so aus:

2.2 Der Schieberegler für den Farbwert

Neben das Farbfeld basteln wir den Schieberegler für die Grundfarbe: Dafür wird ein weiteres <div> mit der id="slider" in den #colorpicker eingefügt:

HTML

<div id="colorpicker">
  <!-- #basecolor, #saturation, #brightness, s.o. -->
  <div id="hue"></div>
</div>

Der #hue-Slider wird schmaler, ebenfalls absolut positioniert und mit einem etwas exotischeren Farbverlauf gestaltet: Um einmal „um den Farbkreis“ zu kommen, werden die Farben Rot (#ff000), Gelb (#ffff00), Grün (#00ff00), Cyan (#00ffff), Blau (#0000ff), Magenta (#ff00ff) und wieder Rot in gleichgroßen Abstufungen (16.6667%) verteilt:

CSS

#hue {
  height: 300px;
  width: 16px;
  position: absolute;
  top: 10px;
  left:320px;
  background-image: linear-gradient(to bottom,
    #ff0000 0%,
    #ffff00 16.6667%,
    #00ff00 33.3334%,
    #00ffff 50%,
    #0000ff 66.6667%,
    #ff00ff 83.3334%,
    #ff0000 100%);
}

Ta-dah: Der Grundfarben-Schieberegler-Hintergrund:

2.3 Die Ausgabe der Farbwerte

Der Ausgabebereich hat vier Angaben: HSB, HSL, RGB und HEX und zusätzlich eine Vorschau der eingestellten Farbe. Die Angaben für HSB und HSL haben eine Gradangabe zwischen 0° und 360° (für die Position des Farbwertes auf dem Farbkreis) und zwei Prozentangaben zwischen 0% und 100%; RGB hat ebenfalls drei Werte für die drei Kanäle, jeweils zwischen 0 und 255, der HEX-Wert ist eigentlich eine Codierung dieser RGB-Werte, besteht also aus drei Zeichengruppen (die Werte von 0 bis 255 werden hier von 00 bis FF angegeben), die hintereinander in einem sechsstelligen Wert mit vorangestellter # angegeben werden.

Alle Ausgaben erhalten innerhalb eines <div id=“values“> einen eigenen Abschnitt, die Werte stehen in entsprechenden <span>s, in welche die Werte nach einer Benutzendeninteraktion dynamisch neu geschrieben werden können:

HTML

<div id="colorpicker">
  <!-- #basecolor, #saturation, #brightness, s.o. -->
  <!-- #hue, s.o. -->
  <div id="values">
    <div id="hsb"><strong>HSB:</strong><br>hue: <span class="huevalue">0</span>°,<br>saturation: <span class="saturationvalue">0</span>%,<br>brightness: <span class="brightnessvalue">100</span>%</div>
    <div id="hsl"><strong>HSL:</strong><br>hue: <span class="huevalue">0</span>°,<br>saturation: <span class="saturationvalue">100</span>%,<br>brightness: <span class="lightnessvalue">100</span>%</div>
    <div id="rgb"><strong>RGB:</strong><br>red: <span class="redvalue">0</span>,<br>green: <span class="greenvalue">0</span>,<br>blue: <span class="bluevalue">0</span></div>
    <div id="hex"><strong>HEX:</strong> <span class="hexvalue"> #ffffff</span></div>
    <div id="result"></div>
  </div>
</div>

CSS

#values {
  position: absolute;
  height: 300px;
  width: 160px;
  top: 10px;
  left: 350px;
  box-sizing:border-box;
  color: #e1e2e3;
  font-size: .9rem;
}
#values strong {
  color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
#values hr {
  border-bottom-color: rgba(255,255,255,0.25);
  border-top-color: transparent;
  margin: 5px 0;
}
#result {
  height: 30px;
  width: 100%;
  background-color:#fff;
  position: relative;
}

Das Ganze sieht dann so aus:

HSB:
hue: 0°,
saturation: 0%,
brightness: 100%
HSL:
hue: 0°,
saturation: 100%,
brightness: 100%
RGB:
red: 0,
green: 0,
blue: 0
HEX: #ffffff

2.4 Farbwähler und Schieberegler

Um nun den Farbwähler-Kringel und das Schieberegler-Schiebedings zu generieren, kommen sowohl zu #basecolor als auch zu #hue jeweils ein div für die „Aktionsfläche“ (#chooserarea und #sliderarea) und jeweils ein div für die beweglichen Elemente (#chooser und #slider) hinzu. #chooserarea und #sliderarea sind die Bereiche, in denen #chooser und #slider bewegt werden können. Diese Bereiche sind jeweils ein paar Pixel größer, damit die Mitte der beweglichen Elemente auch bis ganz in die Randbereiche der Areas bewegt werden können:

HTML

<div id="colorpicker">
  <div id="basecolor">
    <div id="saturation">
      <div id="brightness">
        <div id="chooserarea">      <!-- Bereich für den Helligkeits- und Sättigungswähler -->
          <div id="chooser"></div>  <!-- Helligkeits- und Sättigungswähler -->
        </div>
      </div>
    </div>
  </div>
  <div id="hue">
    <div id="sliderarea">           <!-- Bereich für den Schieberegler -->
      <div id="slider"></div>       <!-- Schieberegler -->
    </div>
  </div>
  <!-- #values -->
</div>

CSS

#chooserarea {
  position: absolute;
  width: 310px;
  height: 310px;
  top: -5px;
  left: -5px;
}
#chooser {
  width: 10px;
  height: 10px;
  border: 1px solid #fff;
  box-sizing: border-box;
  border-radius: 50%;
  outline: 1px solid #000;
  cursor: move;
}
#sliderarea {
  height: 308px;
  width: 20px;
  position: absolute;
  top: -4px;
  left: -2px;
}
#slider {
  width: 20px;
  left: 0px;
  height: 8px;
  border: 1px solid #fff;
  outline: 1px solid #000;
  box-sizing: border-box;
  border-radius: 3px;
  cursor: n-resize;
}

Damit #chooser und #slider „beweglich“ werden, nutze ich die jQuery-Events .mousedown() (Doku), .mousemove() (Doku), .mouseup() (Doku) & .mouseleave() (Doku).

Zwar wäre die Nutzung der jQuery-Erweiterung jQuery UI, im Speziellen die Interaktion draggable, einfacher. Allerdings besteht ein (für mich) nicht zu lösendes Problem darin, dass bei Klick auf die Interaktionsfläche erst das UI-Element zu der Mausposition bewegt und innerhalb des mousedown-Events die Interaktion draggable greifen müsste. Verschärft wird das dadurch, dass mousedown als event listener auf ein Elternelement des beweglichen (draggable) UI-Elements hört. Allerdings ist das nur über Umwege möglich, was wiederum viele andere (für mich) unlösbare Baustellen aufreißt 🙄

Daher ohne jQuery UI.

Um das mousedown- und das mousemove-Event zu kombinieren (z.B. in die freie Fläche der Helligkeitseinstellung zu klicken und direkt zu ziehen), werden die Variablen dragSlider und dragChooser definiert, die standardmäßig auf false stehen. Diese werden bei mousedown mit true überschrieben und bei mouseup und mouseleave wieder auf false gesetzt. Nur, wenn die Variable = true, kann das Event mousemove eine Funktion triggern.

Sowohl mousedown als auch mousemove ermittelt die Position von #chooser (X und Y) bzw. #slider (nur Y) relativ zu der jeweiligen Interaktionsfläche (#chooserarea bzw. #sliderarea). Die Funktionen setChooser() und setSlider() validieren die Koordinaten und setzen diese als Position des jeweiligen UI-Elements via .css().

Die Y-Koordinate des Sliders generiert automatisch die neue Hintergrundfarbe für das Helligkeits- und Sättigungsfeld. Diese Funktion ist hier allerdings erst einmal nur zu Testzwecken implementiert, die wird im weiteren Verlauf noch umgebaut.

JS

$(function() {
  // declare initial variables
  var dragSlider = false,
      sliderY = 0,
      dragChooser = false;
      chooserX = 0,
      chooserY = 0;

  //cklickable and draggable brightness and saturation chooser
  $('#chooserarea').on('mousedown',function(e) {
    dragChooser = true;
    var parentOffset = $('#chooser').parent().offset();
    chooserX = e.pageX - parentOffset.left - 5;
    chooserY = e.pageY - parentOffset.top - 5;
    setChooser();
  }).on('mouseup mouseleave',function() {
    dragChooser = false;
  }).on('mousemove',function(e) {
    if(dragChooser) {
      var parentOffset = $('#chooser').parent().offset();
      chooserX = e.pageX - parentOffset.left - 5;
      chooserY = e.pageY - parentOffset.top - 5;
      setChooser();
    }
  });

  //clickable and draggable hue slider
  $('#sliderarea').on('mousedown',function(e) {
    dragSlider = true;
    var parentOffset = $('#slider').parent().offset();
    sliderY = e.pageY - parentOffset.top;
    setSlider();
  }).on('mouseup mouseleave',function() {
    dragSlider = false;
  }).on('mousemove',function(e) {
    if(dragSlider) {
      var parentOffset = $('#slider').parent().offset();
      sliderY = e.pageY - parentOffset.top;
      setSlider();
    }
  });
  
  //validate negative and too large values, set chooser position on click or drag
  function setChooser() {
    if (chooserX < 0) { chooserX = 0; }
    if (chooserX >300) { chooserX = 300; }
    if (chooserY < 0) { chooserY = 0; }
    if (chooserY >300) { chooserY = 300; }
    $('#chooser').css({'position':'absolute','top':chooserY + 'px','left':chooserX + 'px'});
  }
  //validate negative and too large values, set slider position on click or drag
  function setSlider() {
    if (sliderY < 0) { sliderY = 0; }
    if (sliderY >300) { sliderY = 300; }
    $('#slider').css({'position':'absolute','top':sliderY + 'px'});
    //FOR DEMONSTRATION PURPOSES ONLY
    //convert the slider height (0 - 300px) to hue degrees (0 - 360°) ...
    var baseColor = sliderY * 1.2;
    // ... and set as background-color for #basecolor
    $('#basecolor').css('background-color','hsl(' + baseColor + ',100%,50%)')
  }
});

Das Ergebnis: Die Anzeige für Sättigung und Helligkeit bzw. für Farbwert ist jeweils klickbar, die Position des jeweiligen Wählers wird bei Klick an die Mausposition gesetzt. Innerhalb des mousedown-Events kann der Wähler gezogen werden (Genau dieses ‚dragging‘ innerhalb von mousedown habe ich in jQuery UI nicht hinbekommen. Hinweise willkommen 🙂 ). Das Loslassen des Maus-Buttons (mouseup) und das Herausziehen aus der Interaktionsfläche (mouseleave) führt zum Beenden der entspr. Bewegung des Wählers.

Statt der Farbwerte hier zu Demo-Zwecken die Maus-Events und die (gerundeten) Koordinaten, die man im weiteren Verlauf noch benötigt:

Chooser
mouse event:
touch event:
position x:
position y:

Slider
mouse event:
touch event:
position y:

2.5 Farbwerte be- und umrechnen

Der Rest ist JavaScript und Mathematik:

Speichern der Koordinaten von #chooser und #slider, Berechnen der HSB- und HSL-Werte

  1. Farbwert (hue, HHSB, HSL): Dafür wird die y-Koordinate von #slider benötigt, die oben bereits in der Variable sliderY gespeichert wurde.
    Hier wird sie nun mittels * 1.2 von 0-300px (Position in Px) in 0-360° (Farbwert in Grad) umgerechnet, in die Variable hue geschrieben und abschließend im #values-Abschnitt angezeigt. Da HHSB = HHSL ist, muss nicht umgerechnet werden 😅
    Via .css('background-color','hsl(' + hue + ',100%,50%)'); kann hier bereits die #basecolor gesetzt werden, da man dafür nur den HHSL-Wert benötigt. Die saturation SHSL liegt bei „Vollsättigung“ bei 100%, die lightness LHSL liegt „unabgedunkelt“ bei 50%.
  2. Sättigung (saturation, SHSB): Die x-Koordinate von #chooser wurde oben in der Variable chooserX gespeichert, nun wird sie mittels / 3 von 0-300px (Position in Px) in 0-100% (Sättigung in %) umgerechnet, in die Variable saturationHSB geschrieben und im #values-Abschnitt von #hsb angezeigt.
  3. Helligkeit (brightness, BHSB): Die y-Koordinate von #chooser wurde oben in der Variable chooserY gespeichert, jetzt mittels 100 - (chooserY / 3) von 300-0px (Position) in 0-100% (Helligkeit) umgerechnet, als brightnessHSB gespeichert und ebenfalls im #values-Abschnitt von #hsb angezeigt.
  4. Helligkeit (lightness, LHSL): Wenn man BHSB in LHSL gem. dieser Formel umrechnen möchte, ist
    LHSL = BHSB * (1 – SHSB/2)
    Unter Berücksichtigung der zusätzlich notwendigen Umrechnung der 0-300px in 0-100% ergibt sich untenstehende Formel für lightnessHSL.
  5. Sättigung (saturation, SHSL): Hier wird es noch ein wenig wilder, da unterschiedliche Fälle unterschieden werden:
    • Wenn LHSL 0 oder 100%, dann ist SHSL immer 0,
    • ansonsten ist
      SHSL = (BHSB – LHSL) / min (LHSL, 100 – LHSL)
      gerechnet 😲 Das resultieren bei mir in wilden if/else-Verschachtelungen für die Berechnung von saturationHSL.

JS

function setColor() {
  //generate hue from sliderY, 0-300px => 0-360 deg
  hue = Math.round(sliderY * 1.2);
  //display values
  $('#hsb .huevalue').text( hue );
  $('#hsl .huevalue').text( hue );
  // set basecolor via hsl css with saturation 100% and lightness 50%
  $('#basecolor').css('background-color','hsl(' + hue + ',100%,50%)');

  //generate hsb saturation from chooserX, 0-300px = 0-100%
  saturationHSB = Math.round(chooserX / 3);
  //generate hsb brightness from chooserY, 300-0px = 0-100%
  brightnessHSB = Math.round(100 - (chooserY / 3));
  //display values
  $('#hsb .saturationvalue').text( saturationHSB );
  $('#hsb .brightnessvalue').text( brightnessHSB );

  //convert hsb brightness in hsl lightness
  //convert hsb saturation in hsl saturation
  //formulas from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
  lightnessHSL = Math.round(((brightnessHSB/100) * (1 - ((saturationHSB/100) / 2))) * 100);
  if (lightnessHSL == 0 || lightnessHSL == 100) {
    saturationHSL = 0;
  } else {
    if((1 - (lightnessHSL/100)) < (lightnessHSL/100)) {
      var smallerValue = (1- (lightnessHSL/100));
    } else {
      var smallerValue = (lightnessHSL/100);
    }
    saturationHSL = Math.round(((brightnessHSB/100) - (lightnessHSL/100)) / smallerValue * 100);
  };
  //display values
  $('#hsl .lightnessvalue').text( lightnessHSL );
  $('#hsl .saturationvalue').text( saturationHSL );
}

Vorschau färben, HSL nach RGB und RGB nach HEX konvertieren

Das ist tatsächlich etwas abenteuerlich. Da ich die Umrechnung in JS nicht hinbekomme 🙄, gehe ich einen CSS-Umweg:

HSL ist wie erwähnt im Gegensatz zu HSB prinzipiell CSS-fähig: background-color:hsl(h°,s%,l%); funktioniert in allen gängigen Browsern. Das Element #result wird – wie oben bereits #basecolor – entsprechend mit den berechneten HSL-Werten eingefärbt.

Mittels $(element).css('background-color); gibt jQuery dann aber immer (hoffentlich?) die RGB-Werte als rgb([rotwert],[grünwert],[blauwert]) und nicht die HSL-Werte zurück! Diesen Rückgabe-String kann man von rgb( und ) bereinigen und an den Kommata splitten, um an die drei Werte zu gelangen1. Diese RGB-Werte können nun in die drei HEX-Blöcke konvertiert werden2. Die errechneten Werte für R, G und B werden als #RRGGBB im entsprechenden #values-Abschnitt #hex angezeigt.

JS

//generating result preview background color with hsl css
function refreshPreview() {
  $('#result').css('background-color','hsl(' + hue + ',' + saturationHSL + '%,' + lightnessHSL + '%)');
}

//convert hsl to rgb
function getRgbHex() {
  var result = $('#result').css('background-color');
  var rgb = result.replace(/^rgba?\(|\s+|\)$/g,'').split(',');
  for(var i in rgb) {
    var redValue = rgb[0];
    var greenValue = rgb[1];
    var blueValue = rgb[2];
  }
  $('#rgb .redvalue').text( redValue );
  $('#rgb .greenvalue').text( greenValue );
  $('#rgb .bluevalue').text( blueValue );

  //convert rgb to hex format
  var rgb = result;
  function rgb2hex(rgb){
    rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
    return (rgb && rgb.length === 4) ? '#' +
      ('0' + parseInt(rgb[1],10).toString(16)).slice(-2) +
      ('0' + parseInt(rgb[2],10).toString(16)).slice(-2) +
      ('0' + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
  }
  //alert(rgb2hex(rgb));
  $('#hex .hexvalue').text(rgb2hex(rgb));
}

3. Download

Wie immer, hier die Downloads für die eigenen Bastelversuche und Anpassungen:

Das Ganze gibt es als einzelne HTML-Datei mit dem gesamten HTML-, CSS- und JS-Code hier.
Als zip mit externer style.css und script.js sowie aktueller jquery-3.6.0.min.js gibt es das Ganze hier.

Viel Spaß damit 😁

4. Lizenzen und Fußnoten

"Eine Farbwähler-Bastelanleitung" von Martin Smaxwil ist unter einer CC BY 4.0-Lizenz veröffentlicht.
Darüber hinausgehende Hinweise zu Bestandteilen wie Code Snippets u.ä. findet man hier.

1 Den Code habe ich zum Teil von ‚user372551‘, https://stackoverflow.com/a/3752026, CC-by-sa 2.5, Lizenzhinweise.

2 Der Code ist zum Teil von laurent, https://codepen.io/camponogara/pen/EmXmgy, MIT-Lizenz, Lizenzhinweise.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.