@thangk/easythemer
Version:
Easily generate shades from a colour palette for use in your app
199 lines (198 loc) • 7.85 kB
JavaScript
import validateHex from "./classes/validateHex";
import { _HSL, defaultShadeMultipliers, defaultThemeOption, defaultThemeParams } from "./constants";
function limiter(input, limitStringArray) {
const mode = limitStringArray.split("-")[0];
const limit = parseInt(limitStringArray.split("-")[1]);
// for max mode
if (mode === "max")
return input > limit ? limit : input;
if (mode === "min")
return input < limit ? limit : input;
// for neither mode
return input;
}
function getChannelValue({ shadeMultiplier, HSL_channel, HSL_letter, workingObj }) {
const { upperboundDivider, lowerboundDivider, upperboundPadding, lowerboundPadding } = workingObj;
let HSL_letter_MAX;
switch (HSL_letter) {
case "h":
HSL_letter_MAX = _HSL.MAX_H;
break;
case "s":
HSL_letter_MAX = _HSL.MAX_S;
break;
case "l":
HSL_letter_MAX = _HSL.MAX_L;
break;
default:
HSL_letter_MAX = 0;
break;
}
const upperbound = HSL_letter_MAX - HSL_channel;
const lowerbound = HSL_channel;
const upperboundStep = upperboundDivider && upperboundPadding ? Math.round((upperbound / upperboundDivider) * upperboundPadding) : 0;
const lowerboundStep = lowerboundDivider && lowerboundPadding ? Math.round((lowerbound / lowerboundDivider) * lowerboundPadding) : 0;
if (shadeMultiplier > 0)
HSL_channel = limiter(HSL_channel + upperboundStep * shadeMultiplier, `max-${HSL_letter_MAX - 5}`);
if (shadeMultiplier < 0)
HSL_channel = limiter(HSL_channel + lowerboundStep * shadeMultiplier, `min-${0 + 15}`);
return HSL_channel;
}
export function merger(customOptions, defaultOptions) {
const customOptionsKeys = Object.keys(customOptions);
const defaultOptionsKeys = Object.keys(defaultOptions);
// look for same keys to check if they're both the same object type (not the best method but for this 1 case it works)
const hasSameKeys = customOptionsKeys.every((key) => {
if (defaultOptionsKeys.includes(key))
return true;
});
if (hasSameKeys)
return { ...defaultOptions, ...customOptions };
const mergedOptions = {};
for (const key in customOptions) {
mergedOptions[key] = { ...defaultThemeOption, ...customOptions[key] };
}
return mergedOptions;
}
function getBoundSteps({ h, s, l }, inputThemeOption, shadeMultiplier) {
if (inputThemeOption.params_h?.useDefault === false) {
const params = inputThemeOption.params_s?.params;
const workingObj = Object.assign(defaultThemeParams, params);
h = getChannelValue({ shadeMultiplier, HSL_channel: h, HSL_letter: "h", workingObj });
}
if (inputThemeOption.params_s?.useDefault === false) {
const params = inputThemeOption.params_s?.params;
const workingObj = Object.assign(defaultThemeParams, params);
s = getChannelValue({ shadeMultiplier, HSL_channel: s, HSL_letter: "s", workingObj });
}
if (inputThemeOption.params_l?.useDefault === false) {
const params = inputThemeOption.params_l?.params;
const workingObj = Object.assign(defaultThemeParams, params);
l = getChannelValue({ shadeMultiplier, HSL_channel: l, HSL_letter: "l", workingObj });
}
if (inputThemeOption.params_h?.useDefault === true || undefined) {
h = getChannelValue({ shadeMultiplier, HSL_channel: h, HSL_letter: "h", workingObj: defaultThemeParams });
}
if (inputThemeOption.params_s?.useDefault === true || undefined) {
s = getChannelValue({ shadeMultiplier, HSL_channel: s, HSL_letter: "s", workingObj: defaultThemeParams });
}
if (inputThemeOption.params_l?.useDefault === true || undefined) {
l = getChannelValue({ shadeMultiplier, HSL_channel: l, HSL_letter: "l", workingObj: defaultThemeParams });
}
return { h, s, l };
}
function generateShades(input, shades) {
let shadeSet = {};
const { hex, generateShades } = input;
if (!generateShades)
return { normal: hex };
if (!shades)
shades = defaultShadeMultipliers;
for (const [key, value] of Object.entries(shades)) {
if (!value) {
const { h, s, l } = HexToHSL(hex);
shadeSet[key] = hex;
continue;
}
const { h, s, l } = getBoundSteps(HexToHSL(hex), input, value);
shadeSet[key] = HSLToHex({ h, s, l });
}
return shadeSet;
}
// inspired by https://www.jameslmilner.com/posts/converting-rgb-hex-hsl-colors/
function HexToHSL(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) {
throw new Error("Could not parse Hex Color");
}
const rHex = parseInt(result[1], 16);
const gHex = parseInt(result[2], 16);
const bHex = parseInt(result[3], 16);
const r = rHex / 255;
const g = gHex / 255;
const b = bHex / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = (max + min) / 2;
let s = h;
let l = h;
if (max === min) {
// Achromatic
return { h: 0, s: 0, l };
}
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;
h = Math.round(360 * h);
s = Math.round(s * 100);
l = Math.round(l * 100);
return { h, s, l };
}
// inspired by https://www.jameslmilner.com/posts/converting-rgb-hex-hsl-colors/
function HSLToHex(source) {
let { h, s, l } = source;
l /= 100;
const a = (s * Math.min(l, 1 - l)) / 100;
const f = (n) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
.toString(16)
.padStart(2, "0"); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}`;
}
export function useThemer({ customOptions, customShades, }) {
let error = null;
if (!customOptions) {
error = "ERROR: You must provide at least one colour option.";
return { error };
}
const validateHexLog = {};
let isValidationErrorsExist = false;
let counter = 0;
// validate hex code first
for (const [key, value] of Object.entries(customOptions)) {
console.log(`value ${counter++}`, value);
const value1 = new validateHex(value.hex);
if (!value1.isError) {
validateHexLog[value1.getInput] = value1.errorsList;
isValidationErrorsExist = true;
}
}
if (isValidationErrorsExist) {
console.log("validateHexLog", validateHexLog);
return { error: "ERROR: There are some invalid hex code." };
}
const mergedOptions = merger(customOptions, defaultThemeOption);
const mergedShades = customShades
? merger(customShades, defaultShadeMultipliers)
: defaultShadeMultipliers;
let myTheme = {};
if (mergedOptions)
for (const [key, value] of Object.entries(mergedOptions)) {
if (!value.generateShades) {
myTheme[key] = { normal: value.hex };
continue;
}
if (value.generateShades)
myTheme[key] = generateShades(value, mergedShades);
}
return {
themeOptions: mergedOptions,
themeShades: mergedShades,
myTheme,
error,
};
}