2019-03-12

1644

#css

Adam Giese

269

Mar 12, 2019 â‹… 5 min read

Adam Giese
Software Engineer at Under Armour: Connected Fitness. I love to learn and teach all things web dev.

I know youâ€™re here to learn about manipulating colors â€Šâ€”â€Š and weâ€™ll get there. But before we do, we need a baseline understanding of how CSS notates colors. CSS uses two different color models: RGB and HSL. Letâ€™s take a quick look at both.

An initialism for â€śred, green, blue,â€ť RGB consists of three numbers that each signify how much light of its respective color is included in the resulting end color. In CSS, each of these numbers is in the range of 0â€“255 and would be written as comma-separated parameters of the CSS `rgb`

function. For example, `rgb(50,100,0)`

.

RGB is an â€śadditiveâ€ť color system, which means that the higher each number is, the brighter the end color will be. If all values are equal, the color will be grayscale; if all values are zero, the result will be black; and if all values are 255, the result will be white.

Alternatively, you can notate RGB colors using the hexadecimal notation, in which each colorâ€™s integer is converted from base 10 to base 16. For example, `rgb(50,100,0)`

would be `#326400`

.

Although I usually find myself reaching for RGB (particularly hexadecimal) out of habit, I often find that it is hard to read and especially hard to manipulate. Enter HSL.

An initialism for â€śhue, saturation, light,â€ť HSL also consists of three values. The hue value corresponds to the position on the color wheel and is represented by a CSS angle value; most commonly, deg units are used.

Saturation, represented by a percentage, refers to the intensity of the color. When saturation is 100 percent, it is fully colored; the less saturation, the less color, until it reaches grayscale at 0 percent.

Lightness, also represented by a percentage, refers to how bright a color is. â€śRegularâ€ť brightness is 50 percent. A lightness of 100 percent will be pure white, and 0 percent lightness will be pure black, regardless of the hue and saturation values.

I find HSL to be a more intuitive model. Relations between colors are more immediately evident, and manipulation of colors tends to be as simple as tweaking just one of the numbers.

Both the RGB and HSL color models break down a color into various attributes. To convert between the syntaxes, we first need to calculate these attributes.

With the exception of hue, each value we have discussed can be represented as a percentage. Even the RGB values are byte-sized representations of percentages. In the formulas and functions below, these percentages will be represented by decimals between 0 and 1.

I would like to note that I will not cover the math for these in depth; rather, I will briefly go over the original mathematical formula and then convert it into a JavaScript formula.

Lightness is the easiest of the three HSL values to calculate. Mathematically, the formula is displayed as follows, where `M`

is the maximum of the RGB values and `m`

is the minimum:

Here is the same formula as a JavaScript function:

```
const rgbToLightness = (r,g,b) =>
1/2 * (Math.max(r,g,b) + Math.min(r,g,b));
```

Saturation is only slightly more complicated than lightness. If the lightness is either 0 or 1, then the saturation value will be 0. Otherwise, it follows the mathematical formula below, where `L`

represents lightness:

As JavaScript:

```
const rgbToSaturation = (r,g,b) => {
const L = rgbToLightness(r,g,b);
const max = Math.max(r,g,b);
const min = Math.min(r,g,b);
return (L === 0 || L === 1)
? 0
: (max - min)/(1 - Math.abs(2 * L - 1));
};
```

The formula for calculating the hue angle from RGB coordinates is a bit more complex:

As JavaScript:

```
const rgbToHue = (r,g,b) => Math.round(
Math.atan2(
Math.sqrt(3) * (g - b),
2 * r - g - b,
) * 180 / Math.PI
);
```

The multiplication of `180 / Math.PI`

at the end is to convert the result from radians to degrees.

All of these functions can be wrapped into a single utility function:

```
const rgbToHsl = (r,g,b) => {
const lightness = rgbToLightness(r,g,b);
const saturation = rgbToSaturation(r,g,b);
const hue = rgbToHue(r,g,b);
return [hue, saturation, lightness];
}
```

Before jumping into calculating RGB, we need a few prerequisite values.

First is the â€śchromaâ€ť value:

We also have a temporary hue value, whose range we will use to decide which â€śsegmentâ€ť of the hue circle we belong on:

Next, we have an â€śxâ€ť value, which will be used as the middle (second-largest) component value:

We have an â€śmâ€ť value, which is used to adjust each of the values for lightness:

Depending on the hue prime value, the `r`

, `g`

, and `b`

values will map to `C`

, `X`

, and `0`

:

Lastly, we need to map each value to adjust for lightness:

Putting all of this together into a JavaScript function:

`const hslToRgb = (h,s,l) => { const C = (1 - Math.abs(2 * l - 1)) * s; const hPrime = h / 60; const X = C * (1 - Math.abs(hPrime % 2 - 1)); const m = l - C/2; const withLight = (r,g,b) => [r+m, g+m, b+m];`

`if (hPrime <= 1) { return withLight(C,X,0); } else if (hPrime <= 2) { return withLight(X,C,0); } else if (hPrime <= 3) { return withLight(0,C,X); } else if (hPrime <= 4) { return withLight(0,X,C); } else if (hPrime <= 5) { return withLight(X,0,C); } else if (hPrime <= 6) { return withLight(C,0,X); } }`

For ease of access when manipulating their attributes, we will be dealing with a JavaScript object. This can be created by wrapping the previously written functions:

`const rgbToObject = (red,green,blue) => { const [hue, saturation, lightness] = rgbToHsl(red, green, blue); return {red, green, blue, hue, saturation, lightness}; }`

`const hslToObject = (hue, saturation, lightness) => { const [red, green, blue] = hslToRgb(hue, saturation, lightness); return {red, green, blue, hue, saturation, lightness}; }`

I highly encourage you to spend some time playing with this example. Seeing how each of the attributes interacts when you adjust the others can give you a deeper understanding of how the two color models fit together.

Now that we have the ability to convert between color models, letâ€™s look at how we can manipulate these colors!

Each of the color attributes we have covered can be manipulated individually, returning a new color object. For example, we can write a function that rotates the hue angle:

`const rotateHue = rotation => ({hue, ...rest}) => { const modulo = (x, n) => (x % n + n) % n; const newHue = modulo(hue + rotation, 360);`

`return { ...rest, hue: newHue }; }`

The `rotateHue`

function accepts a `rotation`

parameter and returns a new function, which accepts and returns a color object. This allows for the easy creation of new â€śrotationâ€ť functions:

`const rotate30 = rotateHue(30); const getComplementary = rotateHue(180);`

`const getTriadic = color => { const first = rotateHue(120); const second = rotateHue(-120); return [first(color), second(color)]; }`

Along the same lines, you can write functions to `saturate`

or `lighten`

a color â€Šâ€” â€Šor, inversely, `desaturate`

or `darken`

.

`const saturate = x => ({saturation, ...rest}) => ({ ...rest, saturation: Math.min(1, saturation + x), });`

`const desaturate = x => ({saturation, ...rest}) => ({ ...rest, saturation: Math.max(0, saturation - x), });`

`const lighten = x => ({lightness, ...rest}) => ({ ...rest, lightness: Math.min(1, lightness + x) });`

`const darken = x => ({lightness, ...rest}) => ({ ...rest, lightness: Math.max(0, lightness - x) });`

In addition to color manipulation, you can write â€śpredicatesâ€ťâ€Š â€” â€Šthat is, functions that return a Boolean value.

```
const isGrayscale = ({saturation}) => saturation === 0;
const isDark = ({lightness}) => lightness < .5;
```

The JavaScript `[].filter`

method accepts a predicate and returns a new array with all the elements that â€śpass.â€ť The predicates we wrote in the previous section can be used here:

```
const colors = [/* ... an array of color objects ... */];
const isLight = ({lightness}) => lightness > .5;
const lightColors = colors.filter(isLight);
```

To sort an array of colors, you first need to write a â€ścomparatorâ€ť function. This function takes two elements of an array and returns a number to denote the â€świnner.â€ť A positive number indicates that the first element should be sorted first, and a negative indicates the second should be sorted first. A zero value indicates a tie.

For example, here is a function for comparing the lightness of two colors:

`const compareLightness = (a,b) => a.lightness - b.lightness;`

Here is a function that compares saturation:

`const compareSaturation = (a,b) => a.saturation - b.saturation;`

In an effort to prevent duplication in our code, we can write a higher-order function to return a comparison function to compare any attribute:

`const compareAttribute = attribute => (a,b) => a[attribute] - b[attribute];`

`const compareLightness = compareAttribute('lightness'); const compareSaturation = compareAttribute('saturation'); const compareHue = compareAttribute('hue');`

You can average the specific attributes of an array of colors by composing various JavaScript array methods. First, you can calculate the average of an attribute by summing with reduce and dividing by the array length:

```
const colors = [/* ... an array of color objects ... */];
const toSum = (a,b) => a + b;
const toAttribute = attribute => element => element[attribute];
const averageOfAttribute = attribute => array =>
array.map(toAttribute(attribute)).reduce(toSum) / array.length;
```

You can use this to â€śnormalizeâ€ť an array of colors:

`/* ... continuing */`

`const normalizeAttribute = attribute => array => { const averageValue = averageOfAttribute(attribute)(array); const normalize = overwriteAttribute(attribute)(averageValue); return normalize(array); }`

`const normalizeSaturation = normalizeAttribute('saturation'); const normalizeLightness = normalizeAttribute('lightness'); const normalizeHue = normalizeAttribute('hue');`

Colors are an integral part of the web. Breaking down colors into their attributes allows for the smart manipulation of colors and opens the door to all sorts of possibilities.

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If youâ€™re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps â€” start monitoring for free.

Manage file uploads in your Next.js app using UploadThing, a file upload tool to be used in full-stack TypeScript applications.

Explore the latest updates in Storybook 8, focusing on its improved support for Vite 5 as a build tool.

Next.js 13 introduced some new features like support for Suspense, a React feature that lets you delay displaying a component until the children have finished loading.

Angular’s latest update brings greater control over redirects and the ability to define and assign variables within the template.

×

Hey there, want to help make our blog better?