color.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /**
  2. * 颜色生成
  3. */
  4. type RGB = {
  5. r: number;
  6. g: number;
  7. b: number;
  8. };
  9. type HSL = {
  10. h: number;
  11. s: number;
  12. l: number;
  13. };
  14. type HEX =
  15. | "0"
  16. | "1"
  17. | "2"
  18. | "3"
  19. | "4"
  20. | "5"
  21. | "6"
  22. | "7"
  23. | "8"
  24. | "9"
  25. | "A"
  26. | "B"
  27. | "C"
  28. | "D"
  29. | "E"
  30. | "F";
  31. const RGBUnit = 255;
  32. const HEX_MAP: Record<HEX, number> = {
  33. 0: 0,
  34. 1: 1,
  35. 2: 2,
  36. 3: 3,
  37. 4: 4,
  38. 5: 5,
  39. 6: 6,
  40. 7: 7,
  41. 8: 8,
  42. 9: 9,
  43. A: 10,
  44. B: 11,
  45. C: 12,
  46. D: 13,
  47. E: 14,
  48. F: 15,
  49. };
  50. const rgbWhite = {
  51. r: 255,
  52. g: 255,
  53. b: 255,
  54. };
  55. const rgbBlack = {
  56. r: 0,
  57. g: 0,
  58. b: 0,
  59. };
  60. /**
  61. * RGB颜色转HSL颜色值
  62. * @param r 红色值
  63. * @param g 绿色值
  64. * @param b 蓝色值
  65. * @returns { h: [0, 360]; s: [0, 1]; l: [0, 1] }
  66. */
  67. function rgbToHsl(rgb: RGB): HSL {
  68. let { r, g, b } = rgb;
  69. const hsl = {
  70. h: 0,
  71. s: 0,
  72. l: 0,
  73. };
  74. // 计算rgb基数 ∈ [0, 1]
  75. r /= RGBUnit;
  76. g /= RGBUnit;
  77. b /= RGBUnit;
  78. const max = Math.max(r, g, b);
  79. const min = Math.min(r, g, b);
  80. // 计算h
  81. if (max === min) {
  82. hsl.h = 0;
  83. } else if (max === r) {
  84. hsl.h = 60 * ((g - b) / (max - min)) + (g >= b ? 0 : 360);
  85. } else if (max === g) {
  86. hsl.h = 60 * ((b - r) / (max - min)) + 120;
  87. } else {
  88. hsl.h = 60 * ((r - g) / (max - min)) + 240;
  89. }
  90. hsl.h = hsl.h > 360 ? hsl.h - 360 : hsl.h;
  91. // 计算l
  92. hsl.l = (max + min) / 2;
  93. // 计算s
  94. if (hsl.l === 0 || max === min) {
  95. // 灰/白/黑
  96. hsl.s = 0;
  97. } else if (hsl.l > 0 && hsl.l <= 0.5) {
  98. hsl.s = (max - min) / (max + min);
  99. } else {
  100. hsl.s = (max - min) / (2 - (max + min));
  101. }
  102. return hsl;
  103. }
  104. /**
  105. * hsl -> rgb
  106. * @param h [0, 360]
  107. * @param s [0, 1]
  108. * @param l [0, 1]
  109. * @returns RGB
  110. */
  111. function hslToRgb(hsl: HSL): RGB {
  112. const { h, s, l } = hsl;
  113. const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  114. const p = 2 * l - q;
  115. const hUnit = h / 360; // 色相转换为 [0, 1]
  116. const Cr = fillCircleVal(hUnit + 1 / 3);
  117. const Cg = fillCircleVal(hUnit);
  118. const Cb = fillCircleVal(hUnit - 1 / 3);
  119. // 保持 [0, 1] 环状取值
  120. function fillCircleVal(val: number): number {
  121. return val < 0 ? val + 1 : val > 1 ? val - 1 : val;
  122. }
  123. function computedRgb(val: number): number {
  124. let colorVal: number;
  125. if (val < 1 / 6) {
  126. colorVal = p + (q - p) * 6 * val;
  127. } else if (val >= 1 / 6 && val < 1 / 2) {
  128. colorVal = q;
  129. } else if (val >= 1 / 2 && val < 2 / 3) {
  130. colorVal = p + (q - p) * 6 * (2 / 3 - val);
  131. } else {
  132. colorVal = p;
  133. }
  134. return colorVal * 255;
  135. }
  136. return {
  137. r: Number(computedRgb(Cr).toFixed(0)),
  138. g: Number(computedRgb(Cg).toFixed(0)),
  139. b: Number(computedRgb(Cb).toFixed(0)),
  140. };
  141. }
  142. /**
  143. * 16进制颜色转换RGB
  144. * @param color #rrggbb
  145. * @returns RGB
  146. */
  147. function hexToRGB(hex: string): RGB {
  148. hex = hex.toUpperCase();
  149. const hexRegExp = /^#([0-9A-F]{6})$/;
  150. if (!hexRegExp.test(hex)) {
  151. throw new Error("请传入合法的16进制颜色值,eg: #FF0000");
  152. }
  153. const hexValArr = (hexRegExp.exec(hex)?.[1] || "000000").split(
  154. ""
  155. ) as Array<HEX>;
  156. return {
  157. r: HEX_MAP[hexValArr[0]] * 16 + HEX_MAP[hexValArr[1]],
  158. g: HEX_MAP[hexValArr[2]] * 16 + HEX_MAP[hexValArr[3]],
  159. b: HEX_MAP[hexValArr[4]] * 16 + HEX_MAP[hexValArr[5]],
  160. };
  161. }
  162. /**
  163. * rgb 转 16进制
  164. * @param rgb RGB
  165. * @returns #HEX{6}
  166. */
  167. function rgbToHex(rgb: RGB): string {
  168. const HEX_MAP_REVERSE: Record<string, HEX> = {};
  169. for (const key in HEX_MAP) {
  170. HEX_MAP_REVERSE[HEX_MAP[key as HEX]] = key as HEX;
  171. }
  172. function getRemainderAndQuotient(val: number): string {
  173. val = Math.round(val);
  174. return `${HEX_MAP_REVERSE[Math.floor(val / 16)]}${
  175. HEX_MAP_REVERSE[val % 16]
  176. }`;
  177. }
  178. return `#${getRemainderAndQuotient(rgb.r)}${getRemainderAndQuotient(
  179. rgb.g
  180. )}${getRemainderAndQuotient(rgb.b)}`;
  181. }
  182. // hsl 转 16进制
  183. function hslToHex(hsl: HSL): string {
  184. return rgbToHex(hslToRgb(hsl));
  185. }
  186. // 16进制 转 hsl
  187. function hexToHsl(hex: string): HSL {
  188. return rgbToHsl(hexToRGB(hex));
  189. }
  190. // 生成混合色(混黑 + 混白)
  191. function genMixColor(base: string | RGB | HSL): {
  192. DEFAULT: string;
  193. dark: {
  194. 1: string;
  195. 2: string;
  196. 3: string;
  197. 4: string;
  198. 5: string;
  199. 6: string;
  200. 7: string;
  201. 8: string;
  202. 9: string;
  203. };
  204. light: {
  205. 1: string;
  206. 2: string;
  207. 3: string;
  208. 4: string;
  209. 5: string;
  210. 6: string;
  211. 7: string;
  212. 8: string;
  213. 9: string;
  214. };
  215. } {
  216. // 基准色统一转换为RGB
  217. if (typeof base === "string") {
  218. base = hexToRGB(base);
  219. } else if ("h" in base) {
  220. base = hslToRgb(base);
  221. }
  222. // 混合色
  223. function mix(color: RGB, mixColor: RGB, weight: number): RGB {
  224. return {
  225. r: color.r * (1 - weight) + mixColor.r * weight,
  226. g: color.g * (1 - weight) + mixColor.g * weight,
  227. b: color.b * (1 - weight) + mixColor.b * weight,
  228. };
  229. }
  230. return {
  231. DEFAULT: rgbToHex(base),
  232. dark: {
  233. 1: rgbToHex(mix(base, rgbBlack, 0.1)),
  234. 2: rgbToHex(mix(base, rgbBlack, 0.2)),
  235. 3: rgbToHex(mix(base, rgbBlack, 0.3)),
  236. 4: rgbToHex(mix(base, rgbBlack, 0.4)),
  237. 5: rgbToHex(mix(base, rgbBlack, 0.5)),
  238. 6: rgbToHex(mix(base, rgbBlack, 0.6)),
  239. 7: rgbToHex(mix(base, rgbBlack, 0.7)),
  240. 8: rgbToHex(mix(base, rgbBlack, 0.78)),
  241. 9: rgbToHex(mix(base, rgbBlack, 0.85)),
  242. },
  243. light: {
  244. 1: rgbToHex(mix(base, rgbWhite, 0.1)),
  245. 2: rgbToHex(mix(base, rgbWhite, 0.2)),
  246. 3: rgbToHex(mix(base, rgbWhite, 0.3)),
  247. 4: rgbToHex(mix(base, rgbWhite, 0.4)),
  248. 5: rgbToHex(mix(base, rgbWhite, 0.5)),
  249. 6: rgbToHex(mix(base, rgbWhite, 0.6)),
  250. 7: rgbToHex(mix(base, rgbWhite, 0.7)),
  251. 8: rgbToHex(mix(base, rgbWhite, 0.78)),
  252. 9: rgbToHex(mix(base, rgbWhite, 0.85)),
  253. },
  254. };
  255. }
  256. export {
  257. genMixColor,
  258. rgbToHsl,
  259. rgbToHex,
  260. hslToRgb,
  261. hslToHex,
  262. hexToRGB,
  263. hexToHsl,
  264. };