) => ColorScheme ,
41 | roleMapping: ColorRoleMapping ,
42 | options?: ColorSchemeOptions (baseColors, colorMapping, noCache)
46 |
47 | return roleMapping(colors)
48 | }
49 |
--------------------------------------------------------------------------------
/src/colorSet.ts:
--------------------------------------------------------------------------------
1 | import palette from './palette'
2 |
3 | import type { ColorMapping, Palette } from './palette'
4 | import type { StringOrNumber } from './utils'
5 |
6 | /** Base colors for palettes */
7 | export type BaseColors = Record
8 |
9 | /** Keyed list of color palettes */
10 | export type ColorSet<
11 | P extends string = string,
12 | K extends StringOrNumber = StringOrNumber,
13 | > = Record >
14 |
15 | /**
16 | * Creates color palettes from their corresponding base
17 | * colors
18 | *
19 | * @param baseColors - keyed list of base colors
20 | * @param colorMapping - function for generating the colors
21 | * @param noCache - option to disable caching of color mapping
22 | * @returns a keyed list of palettes
23 | */
24 | export default function colorSet (
25 | baseColors: BaseColors ,
26 | colorMapping?: ColorMapping {
29 | const colorClasses = Object.keys(baseColors) as P[]
30 |
31 | return colorClasses.reduce
40 | }
41 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as colorScheme } from './colorScheme'
2 | export { default as colorSet } from './colorSet'
3 | export { default as normalize } from './normalize'
4 | export { default as palette } from './palette'
5 |
6 | export { default as analogue } from './mappings/analogue'
7 | export { default as complement } from './mappings/complement'
8 | export { default as lightness } from './mappings/lightness'
9 | export { default as opacity } from './mappings/opacity'
10 | export { default as rotation } from './mappings/rotation'
11 | export { default as saturation } from './mappings/saturation'
12 | export { default as triad } from './mappings/triad'
13 |
14 | export { default as harmony } from './presets/harmony'
15 |
16 | export { default as hsl, isHsl } from './color/hsl'
17 | export { default as rgb, isRgb } from './color/rgb'
18 | export { default as hslToRgb } from './color/transforms/hslToRgb'
19 | export { default as rgbToHsl } from './color/transforms/rgbToHsl'
20 |
21 | export type {
22 | ColorRoleMapping,
23 | ColorScheme,
24 | ColorSchemeOptions,
25 | } from './colorScheme'
26 | export type { BaseColors, ColorSet } from './colorSet'
27 | export type { ColorMapping, Palette } from './palette'
28 | export type { HarmonyBaseColors, HarmonyColors } from './presets/harmony'
29 | export type { HSL } from './color/hsl'
30 | export type { RGB } from './color/rgb'
31 |
--------------------------------------------------------------------------------
/src/mappings/__tests__/analogue.spec.ts:
--------------------------------------------------------------------------------
1 | import analogue from '../analogue'
2 |
3 | describe('analogue', () => {
4 | it('returns a new hex color value with hue rotated in steps of 30˚', () => {
5 | const color = analogue('blue', 1)
6 | expect(color).toBe('#8000FF')
7 | })
8 |
9 | it('rotates the hue in the opposite direction if key is negative', () => {
10 | const color = analogue('blue', -1)
11 | expect(color).toBe('#0080FF')
12 | })
13 |
14 | it('returns the base color if key is not a number', () => {
15 | const color = analogue('blue', 'foo')
16 | expect(color).toBe('blue')
17 | })
18 |
19 | it('returns the base color if key is 0', () => {
20 | const color = analogue('blue', 0)
21 | expect(color).toBe('blue')
22 | })
23 |
24 | it('throws if base color is invalid', () => {
25 | expect(() => {
26 | return analogue('bluish', 10)
27 | }).toThrow()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/mappings/__tests__/complement.spec.ts:
--------------------------------------------------------------------------------
1 | import complement from '../complement'
2 |
3 | describe('complement', () => {
4 | it('returns a new hex color value with hue rotated by 180˚ at index 1', () => {
5 | const color = complement('blue', 1)
6 | expect(color).toBe('#FFFF00')
7 | })
8 |
9 | it('returns a new hex color value with hue rotated by 180˚ + 30˚ at index 2', () => {
10 | const color = complement('blue', 2)
11 | expect(color).toBe('#80FF00')
12 | })
13 |
14 | it('rotates hue in the opposite direction if key is negative', () => {
15 | const color = complement('blue', -2)
16 | expect(color).toBe('#FF8000')
17 | })
18 |
19 | it('returns the base color if key is not a number', () => {
20 | const color = complement('blue', 'foo')
21 | expect(color).toBe('blue')
22 | })
23 |
24 | it('returns the base color if key is 0', () => {
25 | const color = complement('blue', 0)
26 | expect(color).toBe('blue')
27 | })
28 |
29 | it('throws if base color is invalid', () => {
30 | expect(() => {
31 | return complement('bluish', 10)
32 | }).toThrow()
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/mappings/__tests__/lightness.spec.ts:
--------------------------------------------------------------------------------
1 | import lightness from '../lightness'
2 |
3 | describe('lightness', () => {
4 | it('returns a new hex color value with adjusted % lightness', () => {
5 | const color = lightness('blue', 40)
6 | expect(color).toBe('#0000CC')
7 | })
8 |
9 | it('returns the base color if key is not a number', () => {
10 | const color = lightness('blue', 'foo')
11 | expect(color).toBe('blue')
12 | })
13 |
14 | it('sets the L value to 100% if key exceeds 100', () => {
15 | const color = lightness('blue', 150)
16 | expect(color).toBe('#FFFFFF')
17 | })
18 |
19 | it('sets the L value to 0% if key is negative', () => {
20 | const color = lightness('blue', -50)
21 | expect(color).toBe('#000000')
22 | })
23 |
24 | it('supports translucent base color', () => {
25 | const color = lightness('rgba(0, 0, 255, 0.5)', 40)
26 | expect(color).toBe('rgba(0, 0, 204, 0.5)')
27 | })
28 |
29 | it('throws if base color is invalid', () => {
30 | expect(() => {
31 | return lightness('bluish', 10)
32 | }).toThrow()
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/mappings/__tests__/opacity.spec.ts:
--------------------------------------------------------------------------------
1 | import opacity from '../opacity'
2 |
3 | describe('opacity', () => {
4 | it('returns an RGBA value with A equal to key', () => {
5 | const color = opacity('blue', 0.4)
6 | expect(color).toBe('rgba(0, 0, 255, 0.4)')
7 | })
8 |
9 | it('returns the base color if key is not a number', () => {
10 | const color = opacity('blue', 'foo')
11 | expect(color).toBe('blue')
12 | })
13 |
14 | it('discards the A value, and returns RGB value instead, if key equals or exceeds 1', () => {
15 | const color = opacity('blue', 40)
16 | expect(color).toBe('#0000FF')
17 | })
18 |
19 | it('sets the A value to 0 if key is negative', () => {
20 | const color = opacity('blue', -1)
21 | expect(color).toBe('rgba(0, 0, 255, 0)')
22 | })
23 |
24 | it('supports translucent base color', () => {
25 | const color = opacity('rgba(0, 0, 255, 0.5)', 0.1)
26 | expect(color).toBe('rgba(0, 0, 255, 0.1)')
27 | })
28 |
29 | it('throws if base color is invalid', () => {
30 | expect(() => {
31 | return opacity('bluish', 0.4)
32 | }).toThrow()
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/mappings/__tests__/rotation.spec.ts:
--------------------------------------------------------------------------------
1 | import rotation from '../rotation'
2 |
3 | describe('rotation', () => {
4 | it('returns a new hex color value with hue rotated by the specified angle', () => {
5 | const color = rotation('blue', 180)
6 | expect(color).toBe('#FFFF00')
7 | })
8 |
9 | it('rotates the hue in the opposite direction if key is negative', () => {
10 | const color = rotation('blue', -90)
11 | expect(color).toBe('#00FF80')
12 | })
13 |
14 | it('returns the base color if key is not a number', () => {
15 | const color = rotation('blue', 'foo')
16 | expect(color).toBe('blue')
17 | })
18 |
19 | it('returns the base color if key is 0', () => {
20 | const color = rotation('blue', 0)
21 | expect(color).toBe('blue')
22 | })
23 |
24 | it('supports translucent base color', () => {
25 | const color = rotation('rgba(0, 0, 255, 0.5)', 180)
26 | expect(color).toBe('rgba(255, 255, 0, 0.5)')
27 | })
28 |
29 | it('throws if base color is invalid', () => {
30 | expect(() => {
31 | return rotation('bluish', 10)
32 | }).toThrow()
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/mappings/__tests__/saturation.spec.ts:
--------------------------------------------------------------------------------
1 | import saturation from '../saturation'
2 |
3 | describe('saturation', () => {
4 | it('returns a new hex color value with adjusted % saturation', () => {
5 | const color = saturation('blue', 40)
6 | expect(color).toBe('#4D4DB3')
7 | })
8 |
9 | it('returns the base color if key is not a number', () => {
10 | const color = saturation('blue', 'foo')
11 | expect(color).toBe('blue')
12 | })
13 |
14 | it('sets the S value to 100% if key exceeds 100', () => {
15 | const color = saturation('blue', 150)
16 | expect(color).toBe('#0000FF')
17 | })
18 |
19 | it('sets the S value to 0% if key is negative', () => {
20 | const color = saturation('blue', -50)
21 | expect(color).toBe('#808080')
22 | })
23 |
24 | it('supports translucent base color', () => {
25 | const color = saturation('rgba(0, 0, 255, 0.5)', 40)
26 | expect(color).toBe('rgba(77, 77, 179, 0.5)')
27 | })
28 |
29 | it('throws if base color is invalid', () => {
30 | expect(() => {
31 | return saturation('bluish', 10)
32 | }).toThrow()
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/mappings/__tests__/triad.spec.ts:
--------------------------------------------------------------------------------
1 | import triad from '../triad'
2 |
3 | describe('triad', () => {
4 | it('returns a new hex color value with hue rotated in steps of 120˚', () => {
5 | const color = triad('blue', 1)
6 | expect(color).toBe('#FF0000')
7 | })
8 |
9 | it('rotates the hue in the opposite direction if key is negative', () => {
10 | const color = triad('blue', -1)
11 | expect(color).toBe('#00FF00')
12 | })
13 |
14 | it('returns the base color if key is not a number', () => {
15 | const color = triad('blue', 'foo')
16 | expect(color).toBe('blue')
17 | })
18 |
19 | it('returns the base color if key is 0', () => {
20 | const color = triad('blue', 0)
21 | expect(color).toBe('blue')
22 | })
23 |
24 | it('throws if base color is invalid', () => {
25 | expect(() => {
26 | return triad('bluish', 10)
27 | }).toThrow()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/mappings/analogue.ts:
--------------------------------------------------------------------------------
1 | import rotation from './rotation'
2 |
3 | import type { ColorMapping } from '../palette'
4 |
5 | /**
6 | * Generates a color that is analogous to the base color
7 | *
8 | * An analogous color is one that is located adjacent to
9 | * the base color around the color wheel, i.e. at around
10 | * 30˚ angle. It is visually similar to the base.
11 | *
12 | * This mapping function rotates the hue in steps of 30˚.
13 | * A negative `key` value rotates in the opposite
14 | * direction.
15 | *
16 | * @param baseColor
17 | * @param key - rotation steps
18 | * @returns new color value in hex
19 | * @throws if `baseColor` is not a valid color value
20 | */
21 | const analogue: ColorMapping = (baseColor, key) => {
22 | if (typeof key !== 'number' || key === 0) return baseColor
23 |
24 | return rotation(baseColor, 30 * key)
25 | }
26 |
27 | export default analogue
28 |
--------------------------------------------------------------------------------
/src/mappings/complement.ts:
--------------------------------------------------------------------------------
1 | import rotation from './rotation'
2 |
3 | import type { ColorMapping } from '../palette'
4 |
5 | /**
6 | * Generates a color that is complementary to the base color
7 | *
8 | * A complementary color is one that is located at the
9 | * opposite side of the color wheel, i.e. at 180˚ angle.
10 | * This provides excellent color contrast.
11 | *
12 | * This mapping function cycles through multiple sets of
13 | * "double complementary" hue rotation. The algorithm loops
14 | * from 1 to `key`, rotates Hue by 180˚ on every odd
15 | * iteration, and 30˚ on even. A negative `key` value
16 | * rotates in the opposite direction.
17 | *
18 | * @param baseColor
19 | * @param key - rotation steps
20 | * @returns new color value in hex
21 | * @throws if `baseColor` is not a valid color value
22 | */
23 | const complement: ColorMapping = (baseColor, key) => {
24 | if (typeof key !== 'number' || key === 0) return baseColor
25 |
26 | let angle = 0
27 | let direction = key < 0 ? -1 : 1
28 | let i = 0
29 | while (i !== key) {
30 | i += direction
31 |
32 | if (i % 2 !== 0) angle += 180 * direction
33 | else angle += 30 * direction
34 | }
35 |
36 | return rotation(baseColor, angle)
37 | }
38 |
39 | export default complement
40 |
--------------------------------------------------------------------------------
/src/mappings/lightness.ts:
--------------------------------------------------------------------------------
1 | import colorString from '../color/colorString'
2 | import hsl from '../color/hsl'
3 |
4 | import type { ColorMapping } from '../palette'
5 |
6 | /**
7 | * Generates new color value by adjusting the base color's
8 | * lightness (the "L" value in HSL color)
9 | *
10 | * @param baseColor
11 | * @param key - percent lightness [0..100]
12 | * @returns new color value in hex
13 | * @throws if `baseColor` is not a valid color value
14 | */
15 | const lightness: ColorMapping = (baseColor, key) => {
16 | if (typeof key !== 'number') return baseColor
17 |
18 | const base = hsl(baseColor)
19 |
20 | const targetL = Math.min(Math.max(key, 0), 100)
21 |
22 | return colorString({ ...base, l: targetL })
23 | }
24 |
25 | export default lightness
26 |
--------------------------------------------------------------------------------
/src/mappings/opacity.ts:
--------------------------------------------------------------------------------
1 | import colorString from '../color/colorString'
2 | import rgb from '../color/rgb'
3 |
4 | import type { ColorMapping } from '../palette'
5 |
6 | /**
7 | * Generates new color value by adjusting the base color's
8 | * opacity (the alpha or "A" value in RGBA)
9 | *
10 | * @param baseColor
11 | * @param key - opacity value [0..1]
12 | * @returns new color value in `rgba(...)` format
13 | * @throws if `baseColor` is not a valid color value
14 | */
15 | const opacity: ColorMapping = (baseColor, key) => {
16 | if (typeof key !== 'number') return baseColor
17 |
18 | const base = rgb(baseColor)
19 |
20 | const targetA = Math.min(Math.max(key, 0), 1)
21 |
22 | return colorString({ ...base, a: targetA })
23 | }
24 |
25 | export default opacity
26 |
--------------------------------------------------------------------------------
/src/mappings/rotation.ts:
--------------------------------------------------------------------------------
1 | import colorString from '../color/colorString'
2 | import hsl from '../color/hsl'
3 |
4 | import type { ColorMapping } from '../palette'
5 |
6 | /**
7 | * Rotates the hue of the base color by a specified angle
8 | * around the color wheel
9 | *
10 | * A negative `key` value reverses the direction of rotation.
11 | *
12 | * @param baseColor
13 | * @param key - rotation angle in degrees
14 | * @returns new color value in hex
15 | * @throws if `baseColor` is not a valid color value
16 | */
17 | const rotation: ColorMapping = (baseColor, key) => {
18 | if (typeof key !== 'number' || key === 0) return baseColor
19 |
20 | const base = hsl(baseColor)
21 |
22 | const targetH = (base.h + key) % 360
23 |
24 | return colorString({ ...base, h: targetH })
25 | }
26 |
27 | export default rotation
28 |
--------------------------------------------------------------------------------
/src/mappings/saturation.ts:
--------------------------------------------------------------------------------
1 | import colorString from '../color/colorString'
2 | import hsl from '../color/hsl'
3 |
4 | import type { ColorMapping } from '../palette'
5 |
6 | /**
7 | * Generates new color value by adjusting the base color's
8 | * saturation (the "S" value in HSL color)
9 | *
10 | * @param baseColor
11 | * @param key - percent saturation [0..100]
12 | * @returns new color value in hex
13 | * @throws if `baseColor` is not a valid color value
14 | */
15 | const saturation: ColorMapping = (baseColor, key) => {
16 | if (typeof key !== 'number') return baseColor
17 |
18 | const base = hsl(baseColor)
19 |
20 | const targetS = Math.min(Math.max(key, 0), 100)
21 |
22 | return colorString({ ...base, s: targetS })
23 | }
24 |
25 | export default saturation
26 |
--------------------------------------------------------------------------------
/src/mappings/triad.ts:
--------------------------------------------------------------------------------
1 | import rotation from './rotation'
2 |
3 | import type { ColorMapping } from '../palette'
4 |
5 | /**
6 | * Generates a triadic complementary color to the base color
7 | *
8 | * A triadic palette consists of 3 colors that are equally
9 | * spaced around the color wheel. Therefore, producing a
10 | * triadic complementary color means rotating the hue by
11 | * 120˚ angle. This provides a more subtle contrast.
12 | *
13 | * This mapping function is cyclic. A negative key value
14 | * rotates in the opposite direction.
15 | *
16 | * @param baseColor
17 | * @param key - rotation steps
18 | * @returns new color value in hex
19 | */
20 | const triad: ColorMapping = (baseColor, key) => {
21 | if (typeof key !== 'number' || key === 0) return baseColor
22 |
23 | return rotation(baseColor, 120 * key)
24 | }
25 |
26 | export default triad
27 |
--------------------------------------------------------------------------------
/src/normalize.ts:
--------------------------------------------------------------------------------
1 | import colorString from './color/colorString'
2 | import rgb from './color/rgb'
3 |
4 | /**
5 | * Normalizes the color string format into either hex or
6 | * rgba
7 | *
8 | * If the color is translucent, i.e. alpha/opacity value
9 | * is less than 1, it is returned as rgba. The 8-digit hex
10 | * format is not preferred, to support older browsers.
11 | *
12 | * @param color
13 | * @returns normalized color string
14 | * @throws if `color` is not a valid color value
15 | */
16 | export default function normalize(color: string): string {
17 | return colorString(rgb(color))
18 | }
19 |
--------------------------------------------------------------------------------
/src/palette.ts:
--------------------------------------------------------------------------------
1 | import lightness from './mappings/lightness'
2 |
3 | import type { StringOrNumber } from './utils'
4 |
5 | /** Function for generating palette colors from base color */
6 | export type ColorMapping