sforkowany z mirror/soapbox
Colors: add HSL utils module
rodzic
4ee9419402
commit
0fd8209df6
|
@ -0,0 +1,82 @@
|
|||
import {
|
||||
hslShift,
|
||||
expandPalette,
|
||||
// paletteToDelta,
|
||||
HSLPaletteDelta,
|
||||
} from '../hsl';
|
||||
|
||||
test('hslShift()', () => {
|
||||
expect(hslShift({ h: 50, s: 50, l: 50 }, [-200, 10, 0]))
|
||||
.toEqual({ h: 210, s: 60, l: 50 });
|
||||
|
||||
expect(hslShift({ h: 1, s: 2, l: 3 }, [0, 0, 0]))
|
||||
.toEqual({ h: 1, s: 2, l: 3 });
|
||||
|
||||
expect(hslShift({ h: 0, s: 100, l: 0 }, [-1, 1, -1]))
|
||||
.toEqual({ h: 359, s: 100, l: 0 });
|
||||
});
|
||||
|
||||
test('expandPalette()', () => {
|
||||
const palette = {
|
||||
200: { h: 7, s: 7, l: 7 },
|
||||
500: { h: 50, s: 50, l: 50 },
|
||||
};
|
||||
|
||||
const paletteDelta: HSLPaletteDelta = {
|
||||
50: [-10, -100, 7],
|
||||
100: [0, 0, 0],
|
||||
200: [0, 0, 0],
|
||||
300: [0, 0, 0],
|
||||
400: [0, 0, 0],
|
||||
500: [0, 0, 0],
|
||||
600: [0, 0, 0],
|
||||
700: [0, 0, 0],
|
||||
800: [0, 0, 0],
|
||||
900: [10, 100, -7],
|
||||
};
|
||||
|
||||
const expected = {
|
||||
50: { h: 40, s: 0, l: 57 },
|
||||
100: { h: 50, s: 50, l: 50 },
|
||||
200: { h: 7, s: 7, l: 7 },
|
||||
300: { h: 50, s: 50, l: 50 },
|
||||
400: { h: 50, s: 50, l: 50 },
|
||||
500: { h: 50, s: 50, l: 50 },
|
||||
600: { h: 50, s: 50, l: 50 },
|
||||
700: { h: 50, s: 50, l: 50 },
|
||||
800: { h: 50, s: 50, l: 50 },
|
||||
900: { h: 60, s: 100, l: 43 },
|
||||
};
|
||||
|
||||
expect(expandPalette(palette, paletteDelta)).toEqual(expected);
|
||||
});
|
||||
|
||||
// test('paletteToDelta()', () => {
|
||||
// const palette = {
|
||||
// 50: { h: 40, s: 0, l: 57 },
|
||||
// 100: { h: 50, s: 50, l: 50 },
|
||||
// 200: { h: 7, s: 7, l: 7 },
|
||||
// 300: { h: 50, s: 50, l: 50 },
|
||||
// 400: { h: 50, s: 50, l: 50 },
|
||||
// 500: { h: 50, s: 50, l: 50 },
|
||||
// 600: { h: 50, s: 50, l: 50 },
|
||||
// 700: { h: 50, s: 50, l: 50 },
|
||||
// 800: { h: 50, s: 50, l: 50 },
|
||||
// 900: { h: 60, s: 100, l: 43 },
|
||||
// };
|
||||
|
||||
// const expected = {
|
||||
// 50: [-10, -50, 7],
|
||||
// 100: [0, 0, 0],
|
||||
// 200: [-43, -43, -43],
|
||||
// 300: [0, 0, 0],
|
||||
// 400: [0, 0, 0],
|
||||
// 500: [0, 0, 0],
|
||||
// 600: [0, 0, 0],
|
||||
// 700: [0, 0, 0],
|
||||
// 800: [0, 0, 0],
|
||||
// 900: [10, 50, -7],
|
||||
// };
|
||||
|
||||
// expect(paletteToDelta(palette)).toEqual(expected);
|
||||
// });
|
|
@ -0,0 +1,88 @@
|
|||
import type { Hsl } from 'soapbox/types/colors';
|
||||
|
||||
/** A neutral color. */
|
||||
const GRAY: Hsl = { h: 0, s: 50, l: 50 };
|
||||
|
||||
/** Modulo (`%`) in both directions. */
|
||||
// https://stackoverflow.com/a/39740009
|
||||
const wrapAround = (value: number, delta: number, max: number): number => {
|
||||
if (delta >= 0) {
|
||||
return (value + delta) % max;
|
||||
} else {
|
||||
return max - ((max - (value + delta)) % max);
|
||||
}
|
||||
};
|
||||
|
||||
/** Clamp the value within the range of `min` and `max`. */
|
||||
// https://stackoverflow.com/a/47837835
|
||||
const minmax = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
) => Math.min(max, Math.max(min, value));
|
||||
|
||||
/**
|
||||
* Represents an HSL color shift.
|
||||
*
|
||||
* For example, `[-20, 10, 0]` means "-20deg hue, +10% saturation, unchanged lightness".
|
||||
*/
|
||||
type HSLDelta = [hDelta: number, sDelta: number, lDelta: number];
|
||||
|
||||
/** Tailwind color shade. */
|
||||
type Shade = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
||||
/** Tailwind color palette with HSL. */
|
||||
type HSLPalette = Record<Shade, Hsl>;
|
||||
/** Tailwind color palette delta map (in HSL). */
|
||||
type HSLPaletteDelta = Record<Shade, HSLDelta>;
|
||||
|
||||
/** Alter the color by the given delta. */
|
||||
const hslShift = (seed: Hsl, delta: HSLDelta): Hsl => {
|
||||
return {
|
||||
h: wrapAround(seed.h, delta[0], 360),
|
||||
s: minmax(seed.s + delta[1], 0, 100),
|
||||
l: minmax(seed.l + delta[2], 0, 100),
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/** Generate a color palette from a single color. */
|
||||
const generatePalette = (seed: Hsl, paletteDelta: HSLPaletteDelta): HSLPalette => {
|
||||
const shades = Object.keys(paletteDelta) as Shade[];
|
||||
|
||||
return shades.reduce((result: HSLPalette, shade: Shade) => {
|
||||
const delta = paletteDelta[shade];
|
||||
result[shade] = hslShift(seed, delta);
|
||||
return result;
|
||||
}, {} as HSLPalette);
|
||||
};
|
||||
|
||||
/** Expands a partial color palette, filling in the gaps. */
|
||||
const expandPalette = (palette: Partial<HSLPalette>, paletteDelta: HSLPaletteDelta): HSLPalette => {
|
||||
const seed = palette['500'] || GRAY;
|
||||
const generated = generatePalette(seed, paletteDelta);
|
||||
return Object.assign(generated, palette);
|
||||
};
|
||||
|
||||
/** Convert a complete color palette into a delta map. */
|
||||
const paletteToDelta = (palette: HSLPalette): HSLPaletteDelta => {
|
||||
const seed = palette['500'];
|
||||
const shades = Object.keys(palette) as Shade[];
|
||||
|
||||
return shades.reduce((result: HSLPaletteDelta, shade: Shade) => {
|
||||
const color = palette[shade];
|
||||
result[shade] = [
|
||||
wrapAround(color.h, -seed.h, 360),
|
||||
minmax(color.s - seed.s, -100, 100),
|
||||
minmax(color.l - seed.l, -100, 100),
|
||||
];
|
||||
return result;
|
||||
}, {} as HSLPaletteDelta);
|
||||
};
|
||||
|
||||
export {
|
||||
hslShift,
|
||||
expandPalette,
|
||||
paletteToDelta,
|
||||
HSLDelta,
|
||||
HSLPaletteDelta,
|
||||
};
|
Ładowanie…
Reference in New Issue