Color picker con Stimulus

(es)

stimulusjavascript

Hicimos un color picker en open tools que toma una aproximación distinta a mostrar los tonos de un color, no es un cambio radical, pero considera que si ya elegiste un color, te muestre distintas tonalidades de éste para que puedas armar tu UI.

No me declaro un experto en colores, pero trataré de dar una explicación de cómo funciona.

El color se puede interpretar de varias maneras

En la vez solemos usar RGB, en donde cada color se explica como la combinación de Rojos, Verdes y Azules. Esto se usa mucho en computación porque las pantallas tienen pixeles de esos colores. Pero eso no es tan cercano a los humanos, por ejemplo, cuando queremos hacer una paleta de colores para un gráfico, tenemos que ir a mano a construirla porque es difícil armar colores usando las combinaciones RGB.

Hay varios modelos de colores, uno que quizás conozcas es CMYK (cyan, magenta, yellow, black) que se usa en impresión en papel, pero hay otros, como HSV (hue, saturation, value) and HSL (hue, saturation, lightness), nunca he podido entender bien Value, pero HSL es muy cómodo.

Hue es el "color" en si: rojo, azul, verde pero aquí no están cada uno en un lado de un cubo, están todos conectados en una "rueda" y se representan como un ángulo. Si rojo está en 0 grados, azul estará en 120 y morado estará en 60 grados. Saturation es la saturación, mientras más saturado es más vibrante "lleno" de color, lo contrario se puede explicar mejor diciendo que algo en escala de grises es un color des-saturado. Lightness es la cantidad de luminosidad del color, baja luminosidad es oscuro, alta luminosidad es claro. El máximo de luminosidad es blanco, lo contrario es negro.

Para ver las tonalidad o sombras de un color entonces debemos mantener el Hue y cambiar el Lightness.

Por ejemplo, el color Avispa es #FFB700, su valor HSL es hsl(43, 100%, 50%) 100% saturado, 50% de luminosidad. Entonces podemos tener tonos oscuros como hsl(43, 100%, 30%), hsl(43, 100%, 10%)o colores brillantes, como hsl(43, 100%, 70%) o hsl(43, 100%, 80%).

La idea entonces es hacer un color picker que ayude a elegir esos colores, y lo hicimos con Stimulus. Aquí intentaré explicar cada parte importante del código.

Primero, la vista.

.field(data-controller='shades')
input(type='color'
data-shades-target='input'
data-action='shades#drawShades')


.color-shades.visible(data-shades-target='shadeList')
.shade(data-shades-target='shade' data-value='100')
.shade(data-shades-target='shade' data-value='200')
.shade(data-shades-target='shade' data-value='300')
.shade(data-shades-target='shade' data-value='400')
.shade(data-shades-target='shade' data-value='500')
.shade(data-shades-target='shade' data-value='600')
.shade(data-shades-target='shade' data-value='700')
.shade(data-shades-target='shade' data-value='800')
.shade(data-shades-target='shade' data-value='900')

Esto significa que la vista se conectará a ShadesController, hay dos tipos de targets: input y shade, uno lo usaremos como inputTarget y el otro como shadeTargets.

Esta es su estructura básica:

import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"

export default class extends Controller {
static targets = [ 'input', 'shade', 'shadeList' ]

drawShades() {}

fillShades(hslHash) {}

hslArrayForLightness(lightness, hsl) {}

RGBToHSL(r,g,b) {}
}

ShadesController tendrá un método muy especial llamado RGBtoHSL que no cubriré en particular acá, pero que debes saber que si pasas RGBtoHSL(r, g, b) nos entrega [h, s, l]. Lo necesitamos usar porque el <input type='color'> entrega un valor #FFB700. drawShades es el método principal que usaremos, fillShades y hslArrayForLightness son métodos para organizar mejor el código.

drawShades() {
const currentColor = this.inputTarget.value
const rgbRegex = /#(?<r>[0-9A-Fa-f]{2})(?<g>[0-9A-Fa-f]{2})(?<b>[0-9A-Fa-f]{2})/
const rgb = currentColor.match(rgbRegex).groups
const hsl = this.RGBToHSL(rgb.r, rgb.g, rgb.b)
const hslHash = this.hslArrayForLightness(hsl[2], hsl)
this.fillShades(hslHash)
}

La primera parte es poder descomponer el valor RGB usando una expresión regular. Esto pasa #FFB700 a ['FF', 'B7', '00'] y luego usando parseInt('FF', 16) tenemos el número 255.

Luego convertimos los valores RGB a HSL, y luego vamos a hslArrayForLightness.

hslArrayForLightness(lightness, hsl) {
const darkStepSize = lightness / 5
const lightStepSize = (100 - lightness) / 5

let lowLs = [1,2,3,4].map((val) => darkStepSize * val)
let highLs = [1,2,3,4].map((val) => lightness + lightStepSize * val)
return (lowLs.concat([lightness], highLs)).map((l) => {
return { h: hsl[0], s: hsl[1], l }
}).reduce((prev, curr, index)=>{
prev[(900 - index * 100).toString()] = curr
return prev
},{})
}

Esto separa el espacio en valores más claros y valores más oscuros. Si colocamos el valor de luminosidad al medio del espectro, tendremos una interpolación de tonos.

Si el valor de luminosidad es 50%, tendremos valores en 10%, 20%, 30%, 40% a la izquierda y 60%, 70%, 80%, 90% a la derecha. Pero si el valor está fuera de la mitad, digamos 80%, tendremos 4 pasos oscuros bien separados y 4 pasos claros muy juntos.

El valor final de esto queda como un Hash en que cada valor de tonalidad (de 100 a 900) es un valor HSL.

fillShades(hslHash) {
this.shadeTargets.forEach((shade) => {
const hsl = hslHash[parseInt(shade.dataset.value)]
shade.style.backgroundColor = `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`
})
}

Ahora que tenemos el diccionario, podemos usar esos valores en cada target que tenemos. Aquí no tenemos que volver a pasar a RGB porque HTML si soporta HSL.