colors.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. import { clamp } from "./math.js";
  2. /**
  3. * Converts a hex color string in the format `#RRGGBB`, `#RRGGBBAA` (or even `RRGGBB` and `RGB`) to a tuple.
  4. * @returns Returns a tuple array where R, G and B are an integer from 0-255 and alpha is a float from 0 to 1, or undefined if no alpha channel exists.
  5. */
  6. export function hexToRgb(hex: string): [red: number, green: number, blue: number, alpha?: number] {
  7. hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim();
  8. const a = hex.length === 8 || hex.length === 4 ? parseInt(hex.slice(-(hex.length / 4)), 16) / (hex.length === 8 ? 255 : 15) : undefined;
  9. if(!isNaN(Number(a)))
  10. hex = hex.slice(0, -(hex.length / 4));
  11. if(hex.length === 3 || hex.length === 4)
  12. hex = hex.split("").map(c => c + c).join("");
  13. const bigint = parseInt(hex, 16);
  14. const r = (bigint >> 16) & 255;
  15. const g = (bigint >> 8) & 255;
  16. const b = bigint & 255;
  17. return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), typeof a === "number" ? clamp(a, 0, 1) : undefined];
  18. }
  19. /** Converts RGB or RGBA number values to a hex color string in the format `#RRGGBB` or `#RRGGBBAA` */
  20. export function rgbToHex(red: number, green: number, blue: number, alpha?: number, withHash = true, upperCase = false): string {
  21. const toHexVal = (n: number) =>
  22. clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")[(upperCase ? "toUpperCase" : "toLowerCase")]();
  23. return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}${alpha ? toHexVal(alpha * 255) : ""}`;
  24. }
  25. /**
  26. * Lightens a CSS color value (in #HEX, rgb() or rgba() format) by a given percentage.
  27. * Will not exceed the maximum range (00-FF or 0-255).
  28. * @returns Returns the new color value in the same format as the input
  29. * @throws Throws if the color format is invalid or not supported
  30. */
  31. export function lightenColor(color: string, percent: number, upperCase = false): string {
  32. return darkenColor(color, percent * -1, upperCase);
  33. }
  34. /**
  35. * Darkens a CSS color value (in #HEX, rgb() or rgba() format) by a given percentage.
  36. * Will not exceed the maximum range (00-FF or 0-255).
  37. * @returns Returns the new color value in the same format as the input
  38. * @throws Throws if the color format is invalid or not supported
  39. */
  40. export function darkenColor(color: string, percent: number, upperCase = false): string {
  41. color = color.trim();
  42. const darkenRgb = (r: number, g: number, b: number, percent: number): [number, number, number] => {
  43. r = Math.max(0, Math.min(255, r - (r * percent / 100)));
  44. g = Math.max(0, Math.min(255, g - (g * percent / 100)));
  45. b = Math.max(0, Math.min(255, b - (b * percent / 100)));
  46. return [r, g, b];
  47. };
  48. let r: number, g: number, b: number, a: number | undefined;
  49. const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/);
  50. if(isHexCol)
  51. [r, g, b, a] = hexToRgb(color);
  52. else if(color.startsWith("rgb")) {
  53. const rgbValues = color.match(/\d+(\.\d+)?/g)?.map(Number);
  54. if(!rgbValues)
  55. throw new Error("Invalid RGB/RGBA color format");
  56. [r, g, b, a] = rgbValues as [number, number, number, number?];
  57. }
  58. else
  59. throw new Error("Unsupported color format");
  60. [r, g, b] = darkenRgb(r, g, b, percent);
  61. if(isHexCol)
  62. return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
  63. else if(color.startsWith("rgba"))
  64. return `rgba(${r}, ${g}, ${b}, ${a ?? NaN})`;
  65. else if(color.startsWith("rgb"))
  66. return `rgb(${r}, ${g}, ${b})`;
  67. else
  68. throw new Error("Unsupported color format");
  69. }