Spis treści

Jak wygenerować kolorowy pasek (js i html canvas)

Komponenty do generowania kolorowych pasków

Dzisiaj generowanie kolorowych pasków. Zapraszam na szybki kodzik.

Zaprogramuję generowanie prostej grafiki - poziomego paska składającego się z pionowych, kolorowych prostokątów, na podstawie wybranego koloru bazowego. Rezultat końcowy wygląda tak:

Przykłady

A tutaj kolejne paski - dla zabawy: 🐁

Jak to napisać?

Canvas

Canvas i kod javascript umieściłam bezpośrednio w treści wpisu (w markdown). Jak na razie wydaje mi się, że wszystko działa. Pod canvasem dodałam selektor koloru bazowego oraz przycisk do przegenerowania paska.

1
2
3
4
<canvas id="bluecanvas" width="1600" height="200">
</canvas>
Kolor bazowy: <input id="bluecanvas_input" type="color" placeholder="" />
<button id="bluecanvas_btn" >Przegeneruj</button>

Sam pasek ma szerokość 100% i wysokość 100px (te właściwości CSS zdefiniowałam w tagu style):

1
2
3
4
5
6
7

<style>
canvas {
width: 100%;
height: 100px;
}
</style>

#Rysowanie

Rysuję kolorowe prostokąty o szerokości losowej od 2 do 20. Funkcja do losowania wartości z przedziału:

1
2
3
function randr(from, to) {
return from + Math.floor(Math.random() * (to - from + 1));
}

Kolory

Konwersja z modelu RGB to HSL

Selektor barw zwraca mi jako wartość DOMString, pobieram więc wartości r, g, b odczytane z selektora przy użyciu funkcji rgbStrToRgbArr(rgbstr):

1
2
3
4
5
6
function rgbStrToRgbArr(rgbstr) {
  let r = '0x' + rgbstr[1] + rgbstr[2];
  let g = '0x' + rgbstr[3] + rgbstr[4];
  let b = '0x' + rgbstr[5] + rgbstr[6];
  return [+r, +g, +b];
}

Otrzymane wartości r, g, b konwertuję na wartości h, s, l. Do przejścia między modelem barw RGB a HSL używam funkcji rgbToHsl(r, g, b) znalezionej tutaj.

HSL dużo bardziej nadaje się do generowania wizualnie atrakcyjnych połączeń barw niż model RGB; właściwość hue w trójce (hue, saturation, lightness) bezpośrednio przekłada się na znane artystom malarzom koło barw, z którego można łatwo odczytać barwy dopełniające czy wygenerować różne “pasujące” barwy.

Generowanie palety

Moja “paleta barw” jest bardzo skromna: to tylko cztery kolory (licząc razem z kolorem bazowym).

Kolory pasków są generowane losowo z puli czterech kolorów: koloru bazowego, koloru dopełniającego (znajdującego się na kole barw 180 stopni dalej) oraz dwóch kolorów oddalonych od bazowego o +30 oraz -30 stopni.

Do stworzenia palety użytam funkcji create_palettes(cs)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Create array of hsl color strings of form hsl(h, s%, l%) from #rrgggbb colorstring
function create_palette(cs) {
let rgbarr = rgbStrToRgbArr(cs);
let [h, s, l] = normalize(rgbToHsl(...rgbarr))
let hue2str = h => hslArrToHslStr([h % 360, s, l])
return [h, h + 180, h - 30, h + 30].map(hue2str);
}

/* Generate string from hsl triple */
function hslArrToHslStr(hslArr) {
return `hsl(${hslArr[0]}, ${hslArr[1]}%, ${hslArr[2]}%)`;
}

/* Convert from [0, 1] ranges to [angle, percent, percent] */
function normalize([h, s, l]) {
  return [h * 360, s * 100, l * 100];
}

Wstawianie wielu pasków na stronę

Deklaracja “komponentu”

Przyznam się, że po wstawieniu kilku bloków htmla zawierających canvas, input i button rozbolał mnie brzuch (zawsze mnie boli jak widzę powtórzenia w kodzie), więc zredukowałam deklarację paska do pojedynczego elementu div z klasą .stripewrap, atrybutem id oraz atrybutem data-color definiującym kolor bazowy.

To taki “mini-komponent”, który będzie utworzony dynamicznie, a potrzebuje jedynie unikalnego (przynajmniej na stronie) id oraz koloru bazowego.

1
<div class="stripewrap" id="pink" data-color="#AA1660"></div>

Oddzielenie konstrukcji i używania

W kodzie javascript wyszukuję elementy po klasie .stripewrap i dynamicznie dodaję do niego elementy: canvas, selektor i przycisk w metodzie generateGuiWithId(idstring) - to etap konstrukcji “bebechów” mojego mini-komponentu.

Następnie te elementy przekazuję do konstruktora new Stripe(canvas, input, button , color) - on statyczną strukturę uczyni żywym, “używalnym” obiektem reagującym na zdarzenia:

1
2
3
4
Array.from(document.getElementsByClassName("stripewrap"))
  .map(e =>
    new Stripe(...generateGuiWithId(e.id), e.dataset["color"])
);

Dynamiczne konstruowanie elementów

A po co to wszystko?

Uparłam się, żeby na stronie było kilka pasków - wtedy łatwo mogę sprawdzić, czy kod związany z “obsługą” paska jest dobrze odizolowany, a samo konstruowanie paska - proste i przyjemne.

Gui paska generuję więc w metodzie generateGuiWithId(idname) wyłącznie na podstawie identyfikatora id znalezionego diva o klasie .stripewrap oraz związanego z nim atrybutu data-color (patrz atrybut dataset).

Całe “gui” to trzy elementy canvas, input i buton, którym ustawiam atrybut id jako połączenie id diva i napisu określającego element (“canvas”, “input” lub “button”):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function generateGuiWithId(idname) {
  var root = document.getElementById(idname);

  var canvas = document.createElement('canvas');
  canvas.id = idname + 'canvas';
  canvas.width = 1600;
  canvas.height = 200;
  
  var input = document.createElement('input');
  input.id = idname + "_input";
  input.type = "color";
  
  var button = document.createElement('button');
  button.id = idname + "_button";
  button.textContent = "Przegeneruj";
  root.append(canvas);
  root.append("Kolor bazowy:");
  root.append(input);
  root.append(button);
  return [canvas, input, button];
}

Obiekt Stripe - obsługa zdarzeń

Po utworzeniu elementów (składowych komponentu) przekazuję je do konstruktora Stripe, który “ożywia” komponent, czyli odpowiada za:

  • zdefiniowanie metody refresh() rysującej paski losowej szerokości i kolorze
  • ustawienie tej metody jako handlera zdarzenia onchange w input oraz onclick w button
  • pierwsze narysowanie paska
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

function Stripe(elem, colinput, btn, initialColor) {
  this.col = initialColor;
  this.elem = elem;
  this.btn = btn;
  this.maxx = elem.width - margin;
  this.stripeHeight = elem.height - 2*margin;
  this.c = elem.getContext('2d');
  this.colinput = colinput;

  this.colinput.onchange = () => this.repaint();
  this.colinput.onchange.bind(this);

  this.btn.onclick = () => this.repaint();
  this.btn.onclick.bind(this);


  this.init = function() {
    this.c.fillStyle = 'black';
    this.c.fillRect(0, 0, this.elem.width, this.elem.height);
    this.colinput.value = this.col;
    this.colinput.placeholder = this.colinput.value;
  }

  this.repaint = function() {
    let cv = this.colinput.value;
    let col_gen = randomize(create_palette(cv));
    let x = margin;
    while (x < this.maxx) {
      let delta = Math.min(randr(minw, maxw), this.maxx - x + 1);
      this.c.fillStyle = col_gen.next().value;
      this.c.fillRect(x, margin, delta, this.stripeHeight);
      x += delta;
    }
  }

  this.init();
  this.repaint(this.colinput.value);
}

I teraz można się w pełni rozkoszować wspaniałymi kolorowymi paskami (definicje kolorów wzięłam z wikipedii):