mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02: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;
 |