diff --git a/docs/api.md b/docs/api.md index fda5dbe4..dac68c8e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -126,6 +126,15 @@ culori.formatRgb('lab(50 0 0 / 25%)'); // ⇒ "rgba(119, 119, 119, 0.25)" ``` +# culori.**formatHsl**(_color_ or _string_) → _string_ · [Source](https://github.com/evercoder/culori/blob/master/src/formatter.js) + +Returns the `hsl(…)` / `hsla(…)` string for a color. Fully opaque colors will be serialized as `hsl()`, and semi-transparent colors as `hsla()`. All values are rounded to a precision of two digits. The Saturation and Lightness are clamped to the interval `[0%, 100%]`. + +```js +culori.formatHsl('lab(50 0 0 / 25%)'); +// ⇒ 'hsla(194.33, 0%, 46.63%, 0.25)' +``` + ## Clamping Some color spaces (Lab and LCh in particular) allow you to express colors that can't be displayed on-screen. The methods below allow you to identify when that's the case and to produce displayable versions of the colors. diff --git a/src/formatter.js b/src/formatter.js index 33832047..82c3de25 100644 --- a/src/formatter.js +++ b/src/formatter.js @@ -1,9 +1,12 @@ import converter from './converter'; import round from './round'; -import fixup from './util/fixup'; let rgb = converter('rgb'); -let roundAlpha = round(2); +let hsl = converter('hsl'); +let twoDecimals = round(2); + +const clamp = value => Math.max(0, Math.min(1, value)); +const fixup = value => Math.round(clamp(value) * 255); const formatHex = c => { let color = rgb(c); @@ -47,7 +50,27 @@ const formatRgb = c => { return `rgb(${r}, ${g}, ${b})`; } else { // transparent color - return `rgba(${r}, ${g}, ${b}, ${roundAlpha(color.alpha)})`; + return `rgba(${r}, ${g}, ${b}, ${twoDecimals(clamp(color.alpha))})`; + } +}; + +const formatHsl = c => { + let color = hsl(c); + + if (color === undefined) { + return undefined; + } + + const h = twoDecimals(color.h || 0); + const s = twoDecimals(clamp(color.s) * 100); + const l = twoDecimals(clamp(color.l) * 100); + + if (color.alpha === undefined || color.alpha === 1) { + // opaque color + return `hsl(${h}, ${s}%, ${l}%)`; + } else { + // transparent color + return `hsla(${h}, ${s}%, ${l}%, ${twoDecimals(clamp(color.alpha))})`; } }; @@ -62,4 +85,4 @@ const formatter = (format = 'rgb') => { return undefined; }; -export { formatHex, formatHex8, formatRgb, formatter }; +export { formatHex, formatHex8, formatRgb, formatHsl, formatter }; diff --git a/src/index.js b/src/index.js index 26833850..c75b9aee 100644 --- a/src/index.js +++ b/src/index.js @@ -113,7 +113,13 @@ export { oklch }; -export { formatter, formatHex, formatHex8, formatRgb } from './formatter'; +export { + formatter, + formatHex, + formatHex8, + formatRgb, + formatHsl +} from './formatter'; export { default as round } from './round'; export { interpolate, diff --git a/src/util/fixup.js b/src/util/fixup.js deleted file mode 100644 index 392c1233..00000000 --- a/src/util/fixup.js +++ /dev/null @@ -1,3 +0,0 @@ -const fixup = value => Math.round(Math.max(0, Math.min(value, 1)) * 255); - -export default fixup; diff --git a/test/formatter.test.js b/test/formatter.test.js index d294475c..6438b261 100644 --- a/test/formatter.test.js +++ b/test/formatter.test.js @@ -1,5 +1,5 @@ import tape from 'tape'; -import { formatHex, formatHex8, formatRgb, rgb } from '../src/index'; +import { formatHex, formatHex8, formatRgb, formatHsl, rgb } from '../src/index'; tape('formatHex', function (test) { test.equal(formatHex('tomato'), '#ff6347'); @@ -20,3 +20,22 @@ tape('formatRgb', function (test) { test.end(); }); + +tape('formatHsl', function (test) { + test.equal(formatHsl('red'), 'hsl(0, 100%, 50%)'); + test.equal( + formatHsl({ + mode: 'hsl', + h: 30.21, + s: 0.2361, + l: 0.48321, + alpha: -0.2 + }), + 'hsla(30.21, 23.61%, 48.32%, 0)' + ); + test.equal( + formatHsl({ mode: 'hsl', h: 405, s: 1.2, l: -1 }), + 'hsl(405, 100%, 0%)' + ); + test.end(); +});