Jak wygenerować kolorowy pasek (js i html canvas)
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.
<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):
<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:
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):
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)
// 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.
<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:
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”):
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
onchangew input orazonclickw button - pierwsze narysowanie paska
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):