Переглянути джерело

feat: color functions changes

Sv443 7 місяців тому
батько
коміт
54ee0ce

+ 5 - 0
.changeset/empty-walls-complain.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": patch
+---
+
+Consolidated behavior of `lightenColor()` and `darkenColor()` when using non-number values

+ 5 - 0
.changeset/healthy-llamas-exist.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": major
+---
+
+Changed `hexToRgb()` and `rgbToHex()` to support hex color codes with an alpha channel

+ 5 - 0
.changeset/silly-mayflies-explode.md

@@ -0,0 +1,5 @@
+---
+"@sv443-network/userutils": minor
+---
+
+Added parameter `upperCase` (false by default) to `lightenColor()` and `darkenColor()`

+ 19 - 12
README.md

@@ -2154,20 +2154,23 @@ The color functions are used to manipulate and convert colors in various formats
 ### hexToRgb()
 Usage:  
 ```ts
-hexToRgb(hex: string): [red: number, green: number, blue: number]
+hexToRgb(hex: string): [red: number, green: number, blue: number, alpha?: number]
 ```  
   
-Converts a hex color string to an RGB color tuple array.  
-Accepts the formats `#RRGGBB` and `#RGB`, with or without the hash symbol.  
+Converts a hex color string to an RGB or RGBA color tuple array.  
+The values of R, G and B will be in the range of 0-255, while the alpha value will be in the range of 0-1.  
+Accepts the formats `#RRGGBB`, `#RRGGBBAA`, `#RGB` and `#RGBA`, with or without the hash symbol.  
   
 <details><summary><b>Example - click to view</b></summary>
 
 ```ts
 import { hexToRgb } from "@sv443-network/userutils";
 
-hexToRgb("#ff0000"); // [255, 0, 0]
-hexToRgb("0032ef");  // [0, 50, 239]
-hexToRgb("#0f0");    // [0, 255, 0]
+hexToRgb("#aaff85aa"); // [170, 255, 133, 0.6666666666666666]
+hexToRgb("#ff0000");   // [255, 0, 0, undefined]
+hexToRgb("0032ef");    // [0, 50, 239, undefined]
+hexToRgb("#0f0");      // [0, 255, 0, undefined]
+hexToRgb("0f0f");      // [0, 255, 0, 1]
 ```
 </details>
 
@@ -2176,7 +2179,7 @@ hexToRgb("#0f0");    // [0, 255, 0]
 ### rgbToHex()
 Usage:  
 ```ts
-rgbToHex(red: number, green: number, blue: number, withHash?: boolean, upperCase?: boolean): string
+rgbToHex(red: number, green: number, blue: number, alpha?: number, withHash?: boolean, upperCase?: boolean): string
 ```  
   
 Converts RGB color values to a hex color string.  
@@ -2188,9 +2191,9 @@ The `upperCase` parameter determines if the output should be in uppercase (false
 ```ts
 import { rgbToHex } from "@sv443-network/userutils";
 
-rgbToHex(255, 0, 0);             // "#ff0000"
-rgbToHex(255, 0, 0, false);      // "ff0000"
-rgbToHex(255, 0, 0, true, true); // "#FF0000"
+rgbToHex(255, 0, 0);                        // "#ff0000" (with hash symbol, lowercase)
+rgbToHex(255, 0, 0, 0.5, false);            // "ff000080" (with alpha, no hash symbol, lowercase)
+rgbToHex(255, 0, 0, undefined, true, true); // "#FF0000" (no alpha, with hash symbol, uppercase)
 ```
 </details>
 
@@ -2199,11 +2202,12 @@ rgbToHex(255, 0, 0, true, true); // "#FF0000"
 ### lightenColor()
 Usage:  
 ```ts
-lightenColor(color: string, percent: number): string
+lightenColor(color: string, percent: number, upperCase?: boolean): string
 ```  
   
 Lightens a CSS color value (in hex, RGB or RGBA format) by a given percentage.  
 Will not exceed the maximum range (00-FF or 0-255).  
+If the `upperCase` parameter is set to true (default is false), the output will be in uppercase.  
 Throws an error if the color format is invalid or not supported.  
   
 <details><summary><b>Example - click to view</b></summary>
@@ -2212,6 +2216,7 @@ Throws an error if the color format is invalid or not supported.
 import { lightenColor } from "@sv443-network/userutils";
 
 lightenColor("#ff0000", 20);              // "#ff3333"
+lightenColor("#ff0000", 20, true);        // "#FF3333"
 lightenColor("rgb(0, 255, 0)", 50);       // "rgb(128, 255, 128)"
 lightenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(128, 255, 128, 0.5)"
 ```
@@ -2222,11 +2227,12 @@ lightenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(128, 255, 128, 0.5)"
 ### darkenColor()
 Usage:  
 ```ts
-darkenColor(color: string, percent: number): string
+darkenColor(color: string, percent: number, upperCase?: boolean): string
 ```  
   
 Darkens a CSS color value (in hex, RGB or RGBA format) by a given percentage.  
 Will not exceed the maximum range (00-FF or 0-255).  
+If the `upperCase` parameter is set to true (default is false), the output will be in uppercase.  
 Throws an error if the color format is invalid or not supported.  
   
 <details><summary><b>Example - click to view</b></summary>
@@ -2235,6 +2241,7 @@ Throws an error if the color format is invalid or not supported.
 import { darkenColor } from "@sv443-network/userutils";
 
 darkenColor("#ff0000", 20);              // "#cc0000"
+darkenColor("#ff0000", 20, true);        // "#CC0000"
 darkenColor("rgb(0, 255, 0)", 50);       // "rgb(0, 128, 0)"
 darkenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(0, 128, 0, 0.5)"
 ```

+ 31 - 23
lib/colors.ts

@@ -1,41 +1,52 @@
 import { clamp } from "./math.js";
 
-/** Converts a hex color string to a tuple of RGB values */
-export function hexToRgb(hex: string): [red: number, green: number, blue: number] {
-  hex = hex.trim();
-  const bigint = parseInt(hex.startsWith("#") ? hex.slice(1) : hex, 16);
+/**
+ * Converts a hex color string in the format `#RRGGBB`, `#RRGGBBAA` (or even `RRGGBB` and `RGB`) to a tuple.  
+ * @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.
+ */
+export function hexToRgb(hex: string): [red: number, green: number, blue: number, alpha?: number] {
+  hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim();
+  const a = hex.length === 8 || hex.length === 4 ? parseInt(hex.slice(-(hex.length / 4)), 16) / (hex.length === 8 ? 255 : 15) : undefined;
+
+  if(!isNaN(Number(a)))
+    hex = hex.slice(0, -(hex.length / 4));
+
+  if(hex.length === 3 || hex.length === 4)
+    hex = hex.split("").map(c => c + c).join("");
+
+  const bigint = parseInt(hex, 16);
 
   const r = (bigint >> 16) & 255;
   const g = (bigint >> 8) & 255;
-  const b = (bigint & 255);
+  const b = bigint & 255;
 
-  return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255)];
+  return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), typeof a === "number" ? clamp(a, 0, 1) : undefined];
 }
 
-/** Converts RGB values to a hex color string */
-export function rgbToHex(red: number, green: number, blue: number, withHash = true, upperCase = false): string {
+/** Converts RGB or RGBA number values to a hex color string in the format `#RRGGBB` or `#RRGGBBAA` */
+export function rgbToHex(red: number, green: number, blue: number, alpha?: number, withHash = true, upperCase = false): string {
   const toHexVal = (n: number) =>
     clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")[(upperCase ? "toUpperCase" : "toLowerCase")]();
-  return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}`;
+  return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}${alpha ? toHexVal(alpha * 255) : ""}`;
 }
 
 /**
- * Lightens a CSS color value (in hex, RGB or RGBA format) by a given percentage.  
+ * Lightens a CSS color value (in #HEX, rgb() or rgba() format) by a given percentage.  
  * Will not exceed the maximum range (00-FF or 0-255).
  * @returns Returns the new color value in the same format as the input
  * @throws Throws if the color format is invalid or not supported
  */
-export function lightenColor(color: string, percent: number): string {
-  return darkenColor(color, percent * -1);
+export function lightenColor(color: string, percent: number, upperCase = false): string {
+  return darkenColor(color, percent * -1, upperCase);
 }
 
 /**
- * Darkens a CSS color value (in hex, RGB or RGBA format) by a given percentage.  
+ * Darkens a CSS color value (in #HEX, rgb() or rgba() format) by a given percentage.  
  * Will not exceed the maximum range (00-FF or 0-255).
  * @returns Returns the new color value in the same format as the input
  * @throws Throws if the color format is invalid or not supported
  */
-export function darkenColor(color: string, percent: number): string {
+export function darkenColor(color: string, percent: number, upperCase = false): string {
   color = color.trim();
 
   const darkenRgb = (r: number, g: number, b: number, percent: number): [number, number, number] => {
@@ -47,28 +58,25 @@ export function darkenColor(color: string, percent: number): string {
 
   let r: number, g: number, b: number, a: number | undefined;
 
-  const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/);
+  const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/);
 
   if(isHexCol)
-    [r, g, b] = hexToRgb(color);
+    [r, g, b, a] = hexToRgb(color);
   else if(color.startsWith("rgb")) {
     const rgbValues = color.match(/\d+(\.\d+)?/g)?.map(Number);
-    if (rgbValues)
-      [r, g, b, a] = rgbValues as [number, number, number, number?];
-    else
+    if(!rgbValues)
       throw new Error("Invalid RGB/RGBA color format");
+    [r, g, b, a] = rgbValues as [number, number, number, number?];
   }
   else
     throw new Error("Unsupported color format");
 
   [r, g, b] = darkenRgb(r, g, b, percent);
 
-  const upperCase = color.match(/[A-F]/) !== null;
-
   if(isHexCol)
-    return rgbToHex(r, g, b, color.startsWith("#"), upperCase);
+    return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
   else if(color.startsWith("rgba"))
-    return `rgba(${r}, ${g}, ${b}, ${a})`;
+    return `rgba(${r}, ${g}, ${b}, ${a ?? NaN})`;
   else if(color.startsWith("rgb"))
     return `rgb(${r}, ${g}, ${b})`;
   else