mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-17 03:39:06 +01:00
304 lines
7.5 KiB
JavaScript
304 lines
7.5 KiB
JavaScript
|
const SATURATION_BOUND = [0, 100];
|
||
|
const LIGHTNESS_BOUND = [0, 100];
|
||
|
|
||
|
const pad2 = str => `${str.length === 1 ? '0' : ''}${str}`;
|
||
|
|
||
|
const clamp = (num, min, max) => Math.max(Math.min(num, max), min);
|
||
|
|
||
|
const random = (min, max) => Math.floor(Math.random() * ((max - min) + 1)) + min;
|
||
|
|
||
|
const randomExclude = (min, max, exclude) => {
|
||
|
const r = random(min, max);
|
||
|
|
||
|
for (let i = 0; i < exclude?.length; i++) {
|
||
|
const value = exclude[i];
|
||
|
|
||
|
if (value?.length === 2 && r >= value[0] && r <= value[1]) {
|
||
|
return randomExclude(min, max, exclude);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return r;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Generate hashCode
|
||
|
* @param {string} str
|
||
|
* @return {number}
|
||
|
*/
|
||
|
const hashCode = str => {
|
||
|
const len = str.length;
|
||
|
let hash = 0;
|
||
|
|
||
|
for (let i = 0; i < len; i++) {
|
||
|
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
||
|
hash &= hash; // Convert to 32bit integer
|
||
|
}
|
||
|
|
||
|
return hash;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Clamps `num` within the inclusive `range` bounds
|
||
|
* @param {number} num
|
||
|
* @param {number|Array} range
|
||
|
* @return {number}
|
||
|
*/
|
||
|
const boundHashCode = (num, range) => {
|
||
|
if (typeof range === 'number') {
|
||
|
return range;
|
||
|
}
|
||
|
|
||
|
return (num % Math.abs(range[1] - range[0])) + range[0];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sanitizing the `range`
|
||
|
* @param {number|Array} range
|
||
|
* @param {Array} bound
|
||
|
* @return {number|Array}
|
||
|
*/
|
||
|
const sanitizeRange = (range, bound) => {
|
||
|
if (typeof range === 'number') {
|
||
|
return clamp(Math.abs(range), ...bound);
|
||
|
}
|
||
|
|
||
|
if (range.length === 1 || range[0] === range[1]) {
|
||
|
return clamp(Math.abs(range[0]), ...bound);
|
||
|
}
|
||
|
|
||
|
return [
|
||
|
Math.abs(clamp(range[0], ...bound)),
|
||
|
clamp(Math.abs(range[1]), ...bound),
|
||
|
];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param {number} p
|
||
|
* @param {number} q
|
||
|
* @param {number} t
|
||
|
* @return {number}
|
||
|
*/
|
||
|
const hueToRgb = (p, q, t) => {
|
||
|
if (t < 0) {
|
||
|
t += 1;
|
||
|
} else if (t > 1) {
|
||
|
t -= 1;
|
||
|
}
|
||
|
|
||
|
if (t < 1 / 6) {
|
||
|
return p + ((q - p) * 6 * t);
|
||
|
}
|
||
|
|
||
|
if (t < 1 / 2) {
|
||
|
return q;
|
||
|
}
|
||
|
|
||
|
if (t < 2 / 3) {
|
||
|
return p + ((q - p) * ((2 / 3) - t) * 6);
|
||
|
}
|
||
|
|
||
|
return p;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Converts an HSL color to RGB
|
||
|
* @param {number} h Hue
|
||
|
* @param {number} s Saturation
|
||
|
* @param {number} l Lightness
|
||
|
* @return {Array}
|
||
|
*/
|
||
|
const hslToRgb = (h, s, l) => {
|
||
|
let r;
|
||
|
let g;
|
||
|
let b;
|
||
|
|
||
|
h /= 360;
|
||
|
s /= 100;
|
||
|
l /= 100;
|
||
|
|
||
|
if (s === 0) {
|
||
|
// achromatic
|
||
|
r = g = b = l;
|
||
|
} else {
|
||
|
const q = l < 0.5
|
||
|
? l * (1 + s)
|
||
|
: (l + s) - (l * s);
|
||
|
const p = (2 * l) - q;
|
||
|
|
||
|
r = hueToRgb(p, q, h + (1 / 3));
|
||
|
g = hueToRgb(p, q, h);
|
||
|
b = hueToRgb(p, q, h - (1 / 3));
|
||
|
}
|
||
|
|
||
|
return [
|
||
|
Math.round(r * 255),
|
||
|
Math.round(g * 255),
|
||
|
Math.round(b * 255),
|
||
|
];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Determines whether the RGB color is light or not
|
||
|
* http://www.w3.org/TR/AERT#color-contrast
|
||
|
* @param {number} r Red
|
||
|
* @param {number} g Green
|
||
|
* @param {number} b Blue
|
||
|
* @param {number} differencePoint
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
const rgbIsLight = (r, g, b, differencePoint) => ((r * 299) + (g * 587) + (b * 114)) / 1000 >= differencePoint; // eslint-disable-line max-len
|
||
|
|
||
|
/**
|
||
|
* Converts an HSL color to string format
|
||
|
* @param {number} h Hue
|
||
|
* @param {number} s Saturation
|
||
|
* @param {number} l Lightness
|
||
|
* @return {string}
|
||
|
*/
|
||
|
const hslToString = (h, s, l) => `hsl(${h}, ${s}%, ${l}%)`;
|
||
|
|
||
|
/**
|
||
|
* Converts RGB color to string format
|
||
|
* @param {number} r Red
|
||
|
* @param {number} g Green
|
||
|
* @param {number} b Blue
|
||
|
* @param {string} format Color format
|
||
|
* @return {string}
|
||
|
*/
|
||
|
const rgbFormat = (r, g, b, format) => {
|
||
|
switch (format) {
|
||
|
case 'rgb':
|
||
|
return `rgb(${r}, ${g}, ${b})`;
|
||
|
case 'hex':
|
||
|
default:
|
||
|
return `#${pad2(r.toString(16))}${pad2(g.toString(16))}${pad2(b.toString(16))}`;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Generate unique color from `value`
|
||
|
* @param {string|number} value
|
||
|
* @param {Object} [options={}]
|
||
|
* @param {string} [options.format='hex']
|
||
|
* The color format, it can be one of `hex`, `rgb` or `hsl`
|
||
|
* @param {number|Array} [options.saturation=[50, 55]]
|
||
|
* Determines the color saturation, it can be a number or a range between 0 and 100
|
||
|
* @param {number|Array} [options.lightness=[50, 60]]
|
||
|
* Determines the color lightness, it can be a number or a range between 0 and 100
|
||
|
* @param {number} [options.differencePoint=130]
|
||
|
* Determines the color brightness difference point. We use it to obtain the `isLight` value
|
||
|
* in the output, it can be a number between 0 and 255
|
||
|
* @return {Object}
|
||
|
* @example
|
||
|
*
|
||
|
* ```js
|
||
|
* uniqolor('Hello world!')
|
||
|
* // { color: "#5cc653", isLight: true }
|
||
|
*
|
||
|
* uniqolor('Hello world!', { format: 'rgb' })
|
||
|
* // { color: "rgb(92, 198, 83)", isLight: true }
|
||
|
*
|
||
|
* uniqolor('Hello world!', {
|
||
|
* saturation: 30,
|
||
|
* lightness: [70, 80],
|
||
|
* })
|
||
|
* // { color: "#afd2ac", isLight: true }
|
||
|
*
|
||
|
* uniqolor('Hello world!', {
|
||
|
* saturation: 30,
|
||
|
* lightness: [70, 80],
|
||
|
* differencePoint: 200,
|
||
|
* })
|
||
|
* // { color: "#afd2ac", isLight: false }
|
||
|
* ```
|
||
|
*/
|
||
|
const uniqolor = (value, {
|
||
|
format = 'hex',
|
||
|
saturation = [50, 55],
|
||
|
lightness = [50, 60],
|
||
|
differencePoint = 130,
|
||
|
} = {}) => {
|
||
|
const hash = Math.abs(hashCode(String(value)));
|
||
|
const h = boundHashCode(hash, [0, 360]);
|
||
|
const s = boundHashCode(hash, sanitizeRange(saturation, SATURATION_BOUND));
|
||
|
const l = boundHashCode(hash, sanitizeRange(lightness, LIGHTNESS_BOUND));
|
||
|
const [r, g, b] = hslToRgb(h, s, l);
|
||
|
|
||
|
return {
|
||
|
color: format === 'hsl'
|
||
|
? hslToString(h, s, l)
|
||
|
: rgbFormat(r, g, b, format),
|
||
|
isLight: rgbIsLight(r, g, b, differencePoint),
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Generate random color
|
||
|
* @param {Object} [options={}]
|
||
|
* @param {string} [options.format='hex']
|
||
|
* The color format, it can be one of `hex`, `rgb` or `hsl`
|
||
|
* @param {number|Array} [options.saturation=[50, 55]]
|
||
|
* Determines the color saturation, it can be a number or a range between 0 and 100
|
||
|
* @param {number|Array} [options.lightness=[50, 60]]
|
||
|
* Determines the color lightness, it can be a number or a range between 0 and 100
|
||
|
* @param {number} [options.differencePoint=130]
|
||
|
* Determines the color brightness difference point. We use it to obtain the `isLight` value
|
||
|
* in the output, it can be a number between 0 and 255
|
||
|
* @param {Array} [options.excludeHue]
|
||
|
* Exclude certain hue ranges. For example to exclude red color range: `[[0, 20], [325, 359]]`
|
||
|
* @return {Object}
|
||
|
* @example
|
||
|
*
|
||
|
* ```js
|
||
|
* // Generate random color
|
||
|
* uniqolor.random()
|
||
|
* // { color: "#644cc8", isLight: false }
|
||
|
*
|
||
|
* // Generate a random color with HSL format
|
||
|
* uniqolor.random({ format: 'hsl' })
|
||
|
* // { color: "hsl(89, 55%, 60%)", isLight: true }
|
||
|
*
|
||
|
* // Generate a random color in specific saturation and lightness
|
||
|
* uniqolor.random({
|
||
|
* saturation: 80,
|
||
|
* lightness: [70, 80],
|
||
|
* })
|
||
|
* // { color: "#c7b9da", isLight: true }
|
||
|
*
|
||
|
* // Generate a random color but exclude red color range
|
||
|
* uniqolor.random({
|
||
|
* excludeHue: [[0, 20], [325, 359]],
|
||
|
* })
|
||
|
* // {color: '#53caab', isLight: true}
|
||
|
* ```
|
||
|
*/
|
||
|
uniqolor.random = ({
|
||
|
format = 'hex',
|
||
|
saturation = [50, 55],
|
||
|
lightness = [50, 60],
|
||
|
differencePoint = 130,
|
||
|
excludeHue,
|
||
|
} = {}) => {
|
||
|
saturation = sanitizeRange(saturation, SATURATION_BOUND);
|
||
|
lightness = sanitizeRange(lightness, LIGHTNESS_BOUND);
|
||
|
|
||
|
const h = excludeHue ? randomExclude(0, 359, excludeHue) : random(0, 359);
|
||
|
const s = typeof saturation === 'number'
|
||
|
? saturation
|
||
|
: random(...saturation);
|
||
|
const l = typeof lightness === 'number'
|
||
|
? lightness
|
||
|
: random(...lightness);
|
||
|
const [r, g, b] = hslToRgb(h, s, l);
|
||
|
|
||
|
return {
|
||
|
color: format === 'hsl'
|
||
|
? hslToString(h, s, l)
|
||
|
: rgbFormat(r, g, b, format),
|
||
|
isLight: rgbIsLight(r, g, b, differencePoint),
|
||
|
};
|
||
|
};
|
||
|
|
||
|
export default uniqolor;
|