@martinlaxenaire/color-palette-generator
Version:
A small, lightweight utility helper to generate color palettes based on a given input color.
674 lines (669 loc) • 24.2 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.window = global.window || {}));
})(this, (function (exports) { 'use strict';
var __typeError = (msg) => {
throw TypeError(msg);
};
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
var _hex, _rgb, _hsl, _hsv;
const _ColorModel = class _ColorModel {
/**
* Creates a new ColorModel instance from a {@link HexColor | hex code}.
* @param hexCode The hexadecimal color code.
*/
constructor(hexCode = "#000000") {
/**
* The hexadecimal representation of the color.
* @private
*/
__privateAdd(this, _hex);
/**
* The RGB representation of the color.
* @private
*/
__privateAdd(this, _rgb);
/**
* The HSL representation of the color.
* @private
*/
__privateAdd(this, _hsl);
/**
* The HSV representation of the color.
* @private
*/
__privateAdd(this, _hsv);
this.hex = hexCode;
}
// HEX
/**
* Gets the hexadecimal representation of the color.
*/
get hex() {
return __privateGet(this, _hex);
}
/**
* Sets the hexadecimal color and updates related color representations.
*/
set hex(value) {
__privateSet(this, _hex, value);
__privateSet(this, _rgb, this.hexToRgb(value));
__privateSet(this, _hsl, this.rgbToHsl(this.rgb));
__privateSet(this, _hsv, this.hslToHsv(this.hsl));
}
// RGB
/**
* Gets the RGB representation of the color.
*/
get rgb() {
return __privateGet(this, _rgb);
}
/**
* Sets the RGB color and updates related color representations.
*/
set rgb(value) {
__privateSet(this, _rgb, value);
__privateSet(this, _hex, this.rgbToHex(value));
__privateSet(this, _hsl, this.rgbToHsl(value));
__privateSet(this, _hsv, this.hslToHsv(this.hsl));
}
// HSL
/**
* Gets the HSL representation of the color.
*/
get hsl() {
return __privateGet(this, _hsl);
}
/**
* Sets the HSL color and updates related color representations.
*/
set hsl(value) {
__privateSet(this, _hsl, value);
__privateSet(this, _rgb, this.hslToRgb(value));
__privateSet(this, _hex, this.rgbToHex(this.rgb));
__privateSet(this, _hsv, this.hslToHsv(value));
}
// HSV
/**
* Gets the HSV representation of the color.
*/
get hsv() {
return __privateGet(this, _hsv);
}
/**
* Sets the HSV color and updates related color representations.
*/
set hsv(value) {
__privateSet(this, _hsv, value);
__privateSet(this, _hsl, this.hsvToHsl(value));
__privateSet(this, _rgb, this.hslToRgb(this.hsl));
__privateSet(this, _hex, this.rgbToHex(this.rgb));
}
// HEX & RGB
/**
* Converts an RGB color to a hexadecimal string.
* @param rgb The {@link RGBColor | RGB color}.
* @returns The {@link HexColor | hex representation}.
*/
rgbToHex({ r = 0, g = 0, b = 0 }) {
const toHex = (x) => {
const hex = Math.round(x).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return "#".concat(toHex(r), "").concat(toHex(g), "").concat(toHex(b), "");
}
/**
* Converts a hexadecimal color to an RGB color.
* @param hexCode The {@link HexColor | hex color code}.
* @returns The {@link RGBColor | RGB representation}.
*/
hexToRgb(hexCode) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexCode);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 };
}
// RGB & HSL
/**
* Converts an RGB color to a HSL color.
* @param rgb The {@link RGBColor | RGB color}.
* @returns The {@link HSLColor | HSL representation}.
*/
rgbToHsl({ r = 0, g = 0, b = 0 }) {
r /= 255, g /= 255, b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return { h: Math.floor(h * 360), s: Math.floor(s * 100), l: Math.floor(l * 100) };
}
/**
* Converts an HSL color to an RGB color.
* @param hsl The {@link HSLColor | HSL color}.
* @returns The {@link RGBColor | RGB representation}.
*/
hslToRgb({ h = 0, s = 0, l = 0 }) {
h /= 360;
s /= 100;
l /= 100;
const output = {
r: 0,
g: 0,
b: 0
};
if (s === 0) {
output.r = output.g = output.b = l;
} else {
const hue2rgb = (p2, q2, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
if (t < 1 / 2) return q2;
if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6;
return p2;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
output.r = hue2rgb(p, q, h + 1 / 3);
output.g = hue2rgb(p, q, h);
output.b = hue2rgb(p, q, h - 1 / 3);
}
output.r *= 255;
output.g *= 255;
output.b *= 255;
return output;
}
// HSL & HSV
/**
* Converts a HSL color to a HSV color.
* @param hsl The {@link HSLColor | HSL color}.
* @returns The {@link HSVColor | HSV representation}.
*/
hslToHsv({ h = 0, s = 0, l = 0 }) {
const L = l / 100;
const V = s / 100 * Math.min(L, 1 - L) + L;
return {
h,
s: V ? 100 * (2 - 2 * L / V) : 0,
v: V * 100
};
}
/**
* Converts a HSV color to a HSL color.
* @param hsv The {@link HSVColor | HSV color}.
* @returns The {@link HSLColor | HSL representation}.
*/
hsvToHsl({ h = 0, s = 0, v = 0 }) {
const V = v / 100;
const L = V - V * s / 200;
const m = Math.min(L, 1 - L);
return {
h,
s: m ? 100 * (V - L) / m : 0,
l: L * 100
};
}
// CMYK
/**
* Converts an RGB color to a CMYK color.
* @param hsv The {@link RGBColor | RGB color}.
* @param normalized Whether to normalize the representation. Default to `false`.
* @returns The {@link CMYKColor | CMYK representation}.
*/
rgbToCmyk({ r = 0, g = 0, b = 0 }, normalized = false) {
let c = 1 - r / 255;
let m = 1 - g / 255;
let y = 1 - b / 255;
let k = Math.min(c, Math.min(m, y));
c = (c - k) / (1 - k);
m = (m - k) / (1 - k);
y = (y - k) / (1 - k);
if (!normalized) {
c = Math.round(c * 1e4) / 100;
m = Math.round(m * 1e4) / 100;
y = Math.round(y * 1e4) / 100;
k = Math.round(k * 1e4) / 100;
}
c = isNaN(c) ? 0 : c;
m = isNaN(m) ? 0 : m;
y = isNaN(y) ? 0 : y;
k = isNaN(k) ? 0 : k;
return {
c,
m,
y,
k
};
}
/**
* Converts a CMYK color to an RGB color.
* @param hsv The {@link CMYKColor | CMYK color}.
* @param normalized Whether to normalize the representation. Default to `false`.
* @returns The {@link RGBColor | RGB representation}.
*/
cmykToRgb({ c = 0, m = 0, y = 0, k = 0 }, normalized = false) {
c = c / 100;
m = m / 100;
y = y / 100;
k = k / 100;
c = c * (1 - k) + k;
m = m * (1 - k) + k;
y = y * (1 - k) + k;
let r = 1 - c;
let g = 1 - m;
let b = 1 - y;
if (!normalized) {
r = Math.round(255 * r);
g = Math.round(255 * g);
b = Math.round(255 * b);
}
return {
r,
g,
b
};
}
// UTILS
/**
* Clones the current color instance.
* @returns A new {@link ColorModel} instance with the same color values.
*/
clone() {
return new _ColorModel(this.hex);
}
/**
* Checks whether the given {@link ColorModel} and this {@link ColorModel} are equal.
* @param color {@link ColorModel} to check against.
* @returns Whether the colors are equal.
*/
equals(color) {
return this.hex === color.hex;
}
/**
* Adjusts the saturation of the color in the HSV color space.
* @param saturation The amount to adjust the saturation.
* @param max The maximum saturation value.
* @param min The minimum saturation value.
* @returns The updated color instance.
*/
saturateHsv(saturation = 0, max = 100, min = 0) {
const currentSat = this.hsv.s;
this.hsv.s = currentSat + saturation > max ? max : currentSat + saturation < min ? min : currentSat + saturation;
this.hsl = this.hsvToHsl(this.hsv);
return this;
}
/**
* Adjusts the brightness of the color in the HSV color space.
* @param brightness The amount to adjust the brightness.
* @param max The maximum brightness value.
* @param min The minimum brightness value.
* @returns The updated color instance.
*/
brighten(brightness = 0, max = 100, min = 0) {
const currentBrightness = this.hsv.v;
this.hsv.v = currentBrightness + brightness > max ? max : currentBrightness + brightness < min ? min : currentBrightness + brightness;
this.hsl = this.hsvToHsl(this.hsv);
return this;
}
/**
* Adjusts the saturation of the color in the HSL color space.
* @param saturation The amount to adjust the saturation.
* @param max The maximum allowed saturation value.
* @param min The minimum allowed saturation value.
* @returns The modified color instance.
*/
saturateHsl(saturation = 0, max = 100, min = 0) {
const currentSat = this.hsl.s;
this.hsl.s = currentSat + saturation > max ? max : currentSat + saturation < min ? min : currentSat + saturation;
this.rgb = this.hslToRgb(this.hsl);
return this;
}
/**
* Adjusts the lightness of the color in the HSL color space.
* @param lightness The amount to adjust the lightness.
* @param max The maximum allowed lightness value.
* @param min The minimum allowed lightness value.
* @returns The modified color instance.
*/
lighten(lightness = 0, max = 100, min = 0) {
const currentLightness = this.hsl.l;
this.hsl.l = currentLightness + lightness > max ? max : currentLightness + lightness < min ? min : currentLightness + lightness;
this.rgb = this.hslToRgb(this.hsl);
return this;
}
/**
* Adds a value to the hue, ensuring it remains within the 0-360 degrees range.
* @param h The original hue value.
* @param add The amount to add to the hue.
* @returns The adjusted hue value.
*/
addToHue(h = 0, add = 0) {
return h + add > 360 ? (h + add) % 360 : h + add < 0 ? 360 + h + add : h + add;
}
};
_hex = new WeakMap();
_rgb = new WeakMap();
_hsl = new WeakMap();
_hsv = new WeakMap();
let ColorModel = _ColorModel;
class ColorPaletteGenerator {
/**
* Creates a new {@link ColorPaletteGenerator} instance from the given parameters.
* @param param {@link ColorPaletteParams} used to generate the palette.
*/
constructor({
rand = () => Math.random(),
precision = 4,
hueRange = 180,
// in degrees
baseColor,
// hex code or ColorModel
baseSaturation
} = {}) {
this.rand = rand;
this.precision = precision;
this.hueRange = hueRange;
if (baseColor) {
if (typeof baseColor === "string") {
this.setBaseColor(baseColor, baseSaturation);
} else {
this.baseColor = baseColor;
}
} else {
this.setBaseColor();
this.baseColor.hsv = {
h: Math.round(this.rand() * 100 * 3.6),
s: baseSaturation !== void 0 ? baseSaturation : Math.round(this.rand() * 20 + 65),
v: Math.round(this.rand() * 20 + 65)
};
}
this.generatePalettes();
}
/**
* Sets the {@link baseColor} to use to generate the {@link palettes}.
* @param baseColor {@link ColorModel#hex | ColorModel hexadecimal} representation to use. Default to `#000000`.
* @param baseSaturation Saturation level to use if any.
*/
setBaseColor(baseColor = "#000000", baseSaturation = null) {
this.baseColor = new ColorModel(baseColor);
if (baseSaturation !== void 0) {
this.baseColor.saturateHsv(baseSaturation);
}
}
/**
* Generates the {@link palettes}.
*/
generatePalettes() {
this.palettes = {
base: [],
light: [],
dark: []
};
this.generateBasePalette();
this.generateLightPalette();
this.generateDarkPalette();
}
/**
* Generates the {@link palettes#base | base palette}.
*
* Use the {@link baseColor} to generate `n * 2` colors (where `n` is the {@link precision}) by shifting the hue around the {@link baseColor} by the {@link hueRange}.
*/
generateBasePalette() {
this.palettes.base.push(this.baseColor);
const hueStep = this.hueRange * 0.5 / this.precision;
let endSaturation = this.rand() * 5 + 22.5;
let endValue = this.rand() * 7.5 + 90;
for (let i = 1; i <= this.precision; i++) {
const color = new ColorModel();
const h = color.addToHue(this.baseColor.hsv.h, hueStep * i);
const s = Math.max(
0,
Math.min(100, this.baseColor.hsv.s - i * (this.baseColor.hsv.s - endSaturation) / this.precision)
);
const v = Math.max(
0,
Math.min(100, this.baseColor.hsv.v + i * (endValue - this.baseColor.hsv.v) / this.precision)
);
color.hsv = {
h,
s,
v
};
this.palettes.base.unshift(color);
}
endSaturation = this.rand() * 7.5 + 90;
endValue = this.rand() * 5 + 22.5;
for (let i = 1; i <= this.precision; i++) {
const color = new ColorModel();
const h = color.addToHue(this.baseColor.hsv.h, -hueStep * i);
const s = Math.max(
0,
Math.min(100, this.baseColor.hsv.s + i * (endSaturation - this.baseColor.hsv.s) / this.precision)
);
const v = Math.max(
0,
Math.min(100, this.baseColor.hsv.v - i * (this.baseColor.hsv.v - endValue) / this.precision)
);
color.hsv = {
h,
s,
v
};
this.palettes.base.push(color);
}
}
/**
* Generates the {@link palettes#light | light palette} based on the {@link palettes#base | base palette} by manipulating saturation and brightness.
*/
generateLightPalette() {
const hue = this.rand() * 5 + 7.5;
const saturation = this.rand() * 7.5 + 22.5;
const value = this.rand() * 7.5 + 27.5;
for (let i = 0; i < this.palettes.base.length; i++) {
const color = new ColorModel(this.palettes.base[i].hex);
color.hsv = {
h: color.addToHue(color.hsv.h, -hue),
s: Math.max(0, Math.min(100, color.hsv.s - saturation)),
v: Math.max(0, Math.min(100, color.hsv.v + value))
};
this.palettes.light.push(color);
}
}
/**
* Generates the {@link palettes#dark | dark palette} based on the {@link palettes#base | base palette} by manipulating saturation and brightness.
*/
generateDarkPalette() {
const hue = this.rand() * 5 + 7.5;
const saturation = this.rand() * 7.5 + 22.5;
const value = this.rand() * 7.5 + 27.5;
for (let i = 0; i < this.palettes.base.length; i++) {
const color = new ColorModel(this.palettes.base[i].hex);
color.hsv = {
h: color.addToHue(color.hsv.h, hue),
s: Math.max(0, Math.min(100, color.hsv.s + saturation)),
v: Math.max(0, Math.min(100, color.hsv.v - value))
};
this.palettes.dark.push(color);
}
}
/**
* Get our {@link palettes#base | base palette}.
* @readonly
*/
get basePalette() {
return this.palettes.base;
}
/**
* Get our {@link palettes#light | light palette}.
* @readonly
*/
get lightPalette() {
return this.palettes.light;
}
/**
* Get our {@link palettes#dark | dark palette}.
* @readonly
*/
get darkPalette() {
return this.palettes.dark;
}
/**
* Get all the palettes as an array, in the {@link palettes#light | light palette}, {@link palettes#base | base palette}, and {@link palettes#dark | dark palette} order.
* @readonly
*/
get fullPalette() {
return [...this.palettes.light, ...this.palettes.base, ...this.palettes.dark];
}
/**
* Generates a random {@link ColorPalette} from our generated {@link palettes}.
* @param param Parameters used to generate the random {@link ColorPalette}.
* @param param.length Number of {@link ColorModel} returned. Default to `4`.
* @param param.includeBaseColor Number of {@link ColorModel} Whether to include the {@link baseColor} in the returned {@link ColorPalette}. Default to `false`.
* @param param.filterPasses Whether to remove every other colors before generating the random palette. Used to avoid getting 2 colors too close. Default to `true`.
* @param param.sortByBrightness Whether to sort the returned {@link ColorModel} by brightness/value, from dark to light. Default to `true`.
* @param param.minSaturation Minimum saturation level of the random returned {@link ColorModel}. Default to `0`.
* @param param.maxSaturation Maximum saturation level of the random returned {@link ColorModel}. Default to `100`.
* @returns Random {@link ColorPalette}.
*/
getRandomPalette({
length = 4,
includeBaseColor = false,
filterPasses = true,
sortByBrightness = true,
minBrightness = 0,
maxBrightness = 100,
minSaturation = 0,
maxSaturation = 100
}) {
if (length < 1) length = 1;
let randomPalette = [...this.fullPalette];
randomPalette = randomPalette.filter((c) => c.hsv.v >= minBrightness);
randomPalette = randomPalette.filter((c) => c.hsv.v <= maxBrightness);
randomPalette = randomPalette.filter((c) => c.hsv.s >= minSaturation);
randomPalette = randomPalette.filter((c) => c.hsv.s <= maxSaturation);
if (filterPasses && this.precision > 2) {
for (let i = 0; i < 2; i++) {
randomPalette = randomPalette.filter((c, index) => index % 2 === 1);
}
}
randomPalette.sort(() => this.rand() - 0.5);
randomPalette = randomPalette.slice(0, length);
if (includeBaseColor) {
const hasBaseColor = randomPalette.find((c) => c.equals(this.baseColor));
if (!hasBaseColor) {
randomPalette[0] = this.baseColor;
}
}
if (sortByBrightness) {
randomPalette.sort((a, b) => a.hsv.v - b.hsv.v);
}
return randomPalette;
}
/**
* Generates a distributed {@link ColorPalette} from our generated {@link palettes}, which is an attempt at get evenly/clever distributed colors.
* @param param Parameters used to generate the distributed {@link ColorPalette}.
* @param param.length Number of {@link ColorModel} returned. Default to `4`.
* @param param.includeBaseColor Number of {@link ColorModel} Whether to include the {@link baseColor} in the returned {@link ColorPalette}. Default to `false`.
* @param param.sortByBrightness Whether to sort the returned {@link ColorModel} by brightness/value, from dark to light. Default to `true`.
* @param param.minSaturation Minimum saturation level of the returned {@link ColorModel}. Default to `0`.
* @param param.maxSaturation Maximum saturation level of the returned {@link ColorModel}. Default to `100`.
* @returns Distributed {@link ColorPalette}.
*/
getDistributedPalette({
length = 4,
includeBaseColor = false,
sortByBrightness = true,
minBrightness = 0,
maxBrightness = 100,
minSaturation = 0,
maxSaturation = 100
}) {
let returnedPalette = [];
if (includeBaseColor) {
returnedPalette.push(this.baseColor);
length = Math.max(0, length - 1);
}
let fullPalette = [...this.fullPalette];
fullPalette = fullPalette.filter((c) => c.hsv.v >= minBrightness);
fullPalette = fullPalette.filter((c) => c.hsv.v <= maxBrightness);
fullPalette = fullPalette.filter((c) => c.hsv.s >= minSaturation);
fullPalette = fullPalette.filter((c) => c.hsv.s <= maxSaturation);
if (fullPalette.length <= length + 1) {
fullPalette.sort(() => this.rand() - 0.5);
fullPalette = fullPalette.slice(0, length);
return fullPalette;
}
const darkPalette = fullPalette.filter((c) => c.hsv.v <= 37.5);
const lightPalette = fullPalette.filter((c) => c.hsv.v >= 87.5);
const basePalette = fullPalette.filter((c) => c.hsv.v > 37.5 && c.hsv.v < 87.5);
const getRandomIndex = (array) => {
return Math.floor(this.rand() * array.length);
};
const darkColorIndexes = [];
const lightColorIndexes = [];
const baseColorIndexes = [];
let nbColors = 0;
if (darkPalette.length) {
const darkIterations = Math.min(darkPalette.length, Math.floor(length / 4));
for (let i = 0; i < darkIterations; i++) {
let index = null;
while (index === null || darkColorIndexes.includes(index) || includeBaseColor && darkPalette[index].equals(this.baseColor)) {
index = getRandomIndex(darkPalette);
}
darkColorIndexes.push(index);
nbColors++;
returnedPalette.push(darkPalette[index]);
}
}
if (lightPalette.length) {
const lightIterations = Math.min(lightPalette.length, Math.floor(length / 4));
for (let i = 0; i < lightIterations; i++) {
let index = null;
while (index === null || lightColorIndexes.includes(index) || includeBaseColor && lightPalette[index].equals(this.baseColor)) {
index = getRandomIndex(lightPalette);
}
lightColorIndexes.push(index);
nbColors++;
returnedPalette.push(lightPalette[index]);
}
}
const baseIterations = Math.min(basePalette.length, length - nbColors);
for (let i = 0; i < baseIterations; i++) {
let index = null;
while (index === null || baseColorIndexes.includes(index) || includeBaseColor && basePalette[index].equals(this.baseColor)) {
index = getRandomIndex(basePalette);
}
baseColorIndexes.push(index);
returnedPalette.push(basePalette[index]);
}
if (sortByBrightness) {
returnedPalette.sort((a, b) => a.hsv.v - b.hsv.v);
}
return returnedPalette;
}
}
exports.ColorModel = ColorModel;
exports.ColorPaletteGenerator = ColorPaletteGenerator;
}));