colour/colour_basic.js

//Initialise functions
{
	if (!global.Colour)
		/**
		 * The namespace for all UF/Colour utility functions, typically for static methods.
		 * 
		 * @namespace Colour
		 */
		global.Colour = {};
	
	/**
	 * Converts a single component to hex.
	 * @alias Colour.componentToHex
	 * 
	 * @param {number} arg0_hex_component
	 * 
	 * @returns {string}
	 */
	Colour.componentToHex = function (arg0_hex_component) {
		//Convert from parameters
		let hex = arg0_hex_component.toString(16);
		
		//Return statement
		return (hex.length === 1) ? "0" + hex : hex;
	}
	
	/**
	 * Converts a hex string to RGB.
	 * @alias Colour.convertHexToRGB
	 *
	 * @param {string} arg0_hex
	 * 
	 * @returns {number[]}
	 */
	Colour.convertHexToRGB = function (arg0_hex) {
		//Convert from parameters
		let hex = arg0_hex;
		
		//Internal guard clause if hex is already an array
		if (Array.isArray(hex)) return hex;
		
		//Clear leading #
		hex = hex.replace("#", "");
		
		//Handle shorthand hex like #abc
		if (hex.length === 3)
			hex = hex
			.split("")
			.map((c) => c + c)
			.join("");
		
		//Extract R, G, B
		let r = parseInt(hex.slice(0, 2), 16);
		let g = parseInt(hex.slice(2, 4), 16);
		let b = parseInt(hex.slice(4, 6), 16);
		
		//Return statement
		return [r, g, b];
	};
	
	/**
	 * Converts a hex string to RGBA.
	 * @alias Colour.convertHexToRGBA
	 *
	 * @param {string} arg0_hex
	 * 
	 * @returns {number[]}
	 */
	Colour.convertHexToRGBA = function (arg0_hex) {
		//Convert from parameters
		let hex = arg0_hex;
		
		//Internal guard clause if hex is of type Array
		if (Array.isArray(hex)) return hex;
		
		//Declare local instance variables
		let alpha = 255;
		
		hex = hex.replace("#", "");
		if (hex.length === 3 || hex.length === 4)
			hex = hex.split("").map((c) => c + c).join("");
		
		//Process hex
		let r = parseInt(hex.slice(0, 2), 16);
		let g = parseInt(hex.slice(2, 4), 16);
		let b = parseInt(hex.slice(4, 6), 16);
		
		if (hex.length === 6) //RGB
			//Return statement
			return [r, g, b, 1];
		if (hex.length === 8) //RGBA
			return [r, g, b, parseInt(hex.slice(6, 8), 16)/255 ];
	};
	
	/**
	 * Converts an RGBA Array to hex.
	 * @alias Colour.convertRGBAToHex
	 *
	 * @param {number[]|string} arg0_rgba
	 * 
	 * @returns {string}
	 */
	Colour.convertRGBAToHex = function (arg0_rgba) {
		//Convert from parameters
		let rgba = arg0_rgba;
		
		//Internal guard clause if rgba is of type string and begins with #
		if (typeof rgba === "string" && rgba.startsWith("#")) return rgba;
		
		//Declare local instance variables
		rgba = rgba.slice(); //Copy to avoid mutating input
		
		//Clamp R, G, B values
		rgba[0] = Math.max(0, Math.min(255, rgba[0]));
		rgba[1] = Math.max(0, Math.min(255, rgba[1]));
		rgba[2] = Math.max(0, Math.min(255, rgba[2]));
		
		//Handle alpha
		if (!(rgba[3] === undefined || rgba[3] > 1))
			if (rgba[3] <= 1) rgba[3] = Math.round(rgba[3]*255);
		
		//Return statement; no alpha, just #rrggbb
		return `#${rgba.map((c) => c.toString(16).padStart(2, "0")).join("").toLowerCase()}`;
	};
	
	/**
	 * Converts an RGB Array to a hex string.
	 * @alias Colour.convertRGBToHex
	 *
	 * @param {number[]|string} arg0_rgb
	 * 
	 * @returns {string}
	 */
	Colour.convertRGBToHex = function (arg0_rgb) {
		// Convert from parameters
		let rgb = arg0_rgb;
		
		// Internal guard clause if input is already a hex string
		if (typeof rgb === "string" && rgb.startsWith("#")) return rgb;
		
		// Copy array to avoid mutation
		try {
			rgb = rgb.slice();
		} catch (e) { 
			console.error("RGB:", rgb, e); 
		}
		
		// Clamp each value between 0 and 255
		rgb[0] = Math.max(0, Math.min(255, rgb[0]));
		rgb[1] = Math.max(0, Math.min(255, rgb[1]));
		rgb[2] = Math.max(0, Math.min(255, rgb[2]));
		
		// Return hex string in format #rrggbb
		return `#${rgb
		.map((c) => c.toString(16).padStart(2, "0"))
		.join("")
		.toLowerCase()}`;
	};
	
	/**
	 * Decodes an RGBA pixel to a number.
	 * @alias Colour.decodeRGBAAsNumber
	 * 
	 * @param {number[]} arg0_rgba
	 * 
	 * @returns {number}
	 */
	Colour.decodeRGBAAsNumber = function (arg0_rgba) {
		//Convert from parameters
		let rgba = arg0_rgba;
		
		//Declare local instance variables
		let r = rgba[0];
		let g = rgba[1];
		let b = rgba[2];
		let a = rgba[3];
		
		//Return statement (rebuild 32-bit integer)
		return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
	};
	
	/**
	 * Calculates the deltaE between two RGB values.
	 * @alias Colour.deltaE
	 * 
	 * @param {number[]} arg0_rgb
	 * @param {number[]} arg1_rgb
	 * 
	 * @returns {number}
	 */
	Colour.deltaE = function (arg0_rgb, arg1_rgb) {
		//Convert from parameters
		let labA = Colour.RGBToLab(arg0_rgb);
		let labB = Colour.RGBToLab(arg1_rgb);
		
		//Declare local instance variables
		let deltaL = labA[0] - labB[0];
		let deltaA = labA[1] - labB[1];
		let deltaB = labA[2] - labB[2];
		let c1 = Math.sqrt(labA[1]*labA[1] + labA[2]*labA[2]);
		let c2 = Math.sqrt(labB[1]*labB[1] + labB[2]*labB[2]);
		let deltaC = c1 - c2;
		let deltaH = deltaA*deltaA + deltaB*deltaB - deltaC*deltaC;
		deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
		let sc = 1.0 + 0.045*c1;
		let sh = 1.0 + 0.015*c1;
		let deltaLKlsl = deltaL/(1.0);
		let deltaCkcsc = deltaC/(sc);
		let deltaHkhsh = deltaH/(sh);
		let i = deltaLKlsl*deltaLKlsl + deltaCkcsc*deltaCkcsc + deltaHkhsh*deltaHkhsh;
		
		//Return statement
		return (i < 0) ? 0 : Math.sqrt(i);
	};
	
	/**
	 * Returns a random [R, G, B] colour.
	 * @alias Colour.generateRandomColour
	 * 
	 * @returns {number[]}
	 */
	Colour.generateRandomColour = function () {
		//Return statement
		return [Math.randomNumber(0, 255), Math.randomNumber(0, 255), Math.randomNumber(0, 255)];
	};
	
	/**
	 * Returns the actual [R, G, B, A] colour of a given string. Depends on a browser/HTML context to work.
	 * @alias Colour.getColour
	 * 
	 * @param {any} arg0_colour
	 * 
	 * @returns {number[]|any}
	 */
	Colour.getColour = function (arg0_colour) {
		//Convert from parameters
		let colour = arg0_colour;
		
		if (Array.isArray(colour)) return colour; //Internal guard clause if the colour is already an array
		
		try {
			//Declare local instance variables
			let colour_el = document.createElement("div");
				colour_el.style.color = colour;
			document.body.appendChild(colour_el);	
			
			let actual_colour = window.getComputedStyle(colour_el).color;
				colour_el.remove();
			
			if (actual_colour.startsWith("rgb(") || actual_colour.startsWith("rgba(")) {
				actual_colour = actual_colour.replace("rgb(", "")
					.replace("rgba(", "")
					.replace(")", "");
				actual_colour = actual_colour.split(",");
				
				//Iterate over actual_colour and set it
				for (let i = 0; i < actual_colour.length; i++)
					actual_colour[i] = parseFloat(actual_colour[i].trim());
				if (actual_colour.length >= 4)
					actual_colour[3] *= 255;
			}
			
			//Return statement
			return actual_colour;
		} catch (e) { console.error(e); }
		
		//Return statement
		if (Colour._map[colour]) colour = Colour._map[colour];
		return Colour.convertHexToRGBA(colour);
	};
	
	/**
	 * Returns the best text colour given a certain colour background. Either 'black'/'white'.
	 * @alias Colour.getBestTextColour
	 * 
	 * @param {number[]|string} arg0_colour
	 * @param {Object} [arg1_options]
	 *  @param {boolean} [arg1_options.return_rgb=false]
	 * 
	 * @returns {number[]|string}
	 */
	Colour.getBestTextColour = function (arg0_colour, arg1_options) { //[WIP] - Finish function body
		//Convert from parameters
		let colour = Colour.getColour(arg0_colour);
		let options = (arg1_options) ? arg1_options : {};
		
		//Declare local instance variables
		let luminance = Colour.getLuminance(colour);
		
		let best_colour = (luminance > 0.179) ? "black" : "white";
		
		//Return statement
		if (options.return_rgb) {
			if (best_colour === "black") return [0, 0, 0];
			if (best_colour === "white") return [255, 255, 255];
		}
		return best_colour;
	};
	
	/**
	 * Encodes a number as an RGBA pixel.
	 * @alias Colour.encodeNumberAsRGBA
	 * 
	 * @param {number} arg0_number
	 * @returns {number[]}
	 */
	Colour.encodeNumberAsRGBA = function (arg0_number) {
		//Convert from parameters
		let number = Math.returnSafeNumber(Math.ceil(arg0_number));
		
		//Declare local instance variables
		let r = (number >> 24) & 0xFF; //Extract highest 8 bits
		let g = (number >> 16) & 0xFF; //Extract next 8 bits
		let b = (number >> 8) & 0xFF;  //Extract next 8 bits
		let a = number & 0xFF;         //Extract lowest 8 bits
		
		//Return statement
		return [r, g, b, a];
	};
	
	/**
	 * Returns the luminance of a given colour.
	 * @alias Colour.getLuminance
	 * 
	 * @param {number[]|string} arg0_colour
	 * 
	 * @returns {number}
	 */
	Colour.getLuminance = function (arg0_colour) {
		//Convert from parameters
		let colour = Colour.getColour(arg0_colour);
		
		//Declare local instance variables
		let a = colour.map((v) => {
			v /= 255;
			return v <= 0.03928 ? v/12.92 : Math.pow((v + 0.055)/1.055, 2.4);
		});
		
		//Return statement
		return a[0]*0.2126 + a[1]*0.7152 + a[2]*0.0722;
	};
	
	/**
	 * Returns a random hex colour.
	 * @alias Colour.randomHex
	 * 
	 * @returns {string}
	 */
	Colour.randomHex = function () {
		//Return statement
		return Colour.convertRGBToHex(Colour.randomRGB());
	};
	
	/**
	 * Returns a random RGB colour.
	 * @alias Colour.randomRGBA
	 * 
	 * @returns {number[]}
	 */
	Colour.randomRGB = function () {
		//Return statement
		return [
			Math.randomNumber(0, 255),
			Math.randomNumber(0, 255),
			Math.randomNumber(0, 255)
		];
	};
	
	/**
	 * Returns a random RGBA colour.
	 * 
	 * @returns {number[]}
	 */
	Colour.randomRGBA = function () {
		return [
			Math.randomNumber(0, 255),
			Math.randomNumber(0, 255),
			Math.randomNumber(0, 255),
			Math.randomNumber(0, 255)
		];
	};
	
	/**
	 * Converts an RGB value to Lab distance.
	 * @alias Colour.RGBToLab
	 * 
	 * @param {number[]} arg0_rgb
	 * 
	 * @returns {number[]}
	 */
	Colour.RGBToLab = function (arg0_rgb) {
		//Convert from parameters
		let rgb = arg0_rgb;
		
		//Declare local instance variables
		let r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255, x, y, z;
		r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
		g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
		b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
		x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
		y = (r * 0.2126 + g * 0.7152 + b * 0.0722);
		z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
		x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
		y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
		z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
		
		//Return statement
		return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
	}
}