HSV color picker
I've created a HSV color picker, complete with color wheel. The color picker can also be switched to RGB seamlessly.
Checkout the live demo of Color pickerCode
Sadly I cannot really link any code, as it is not on a repository, but here some snippets:
Get HSV values, based on X / Y
export default function getHSVForPositionOnWheel( x: number, y: number, radius: number, ): [hue: number, saturation: number, value: number] { const distance = Math.sqrt(x * x + y * y); const radian = Math.atan2(y, x); const degrees = ((radian + Math.PI) / (2 * Math.PI)) * 360; return [degrees, distance / radius, 1]; }
Color utility
Here is the color class with meany useful things in it. Feel free to use it for educational purposes.
export default class Color { public static readonly BLACK = new Color(0, 0, 0, 255); public static readonly WHITE = new Color(255, 255, 255, 255); public static readonly HEX_RGBA_REGEX = /#([0-9]|[a-f]){8}/i; /** * Creates a color from HSV * @param hue value from `0` to `360` * @param saturation value from `0` to `1` * @param value value from `0` to `1` * @param alpha value from `0` to `255` */ public static fromHSV( hue: number, saturation: number, value: number, alpha = 255, ): Color { if (!this.isValidHSV(hue, saturation, value)) { throw new Error( `HSV values hue: ${hue}, saturation: ${saturation} value: ${value} are incorrect`, ); } const chroma = saturation * value; const hue1 = hue / 60; const x = chroma * (1 - Math.abs((hue1 % 2) - 1)); const m = value - chroma; let r1, g1, b1; if (hue1 >= 0 && hue1 <= 1) [r1, g1, b1] = [chroma, x, 0]; else if (hue1 >= 1 && hue1 <= 2) [r1, g1, b1] = [x, chroma, 0]; else if (hue1 >= 2 && hue1 <= 3) [r1, g1, b1] = [0, chroma, x]; else if (hue1 >= 3 && hue1 <= 4) [r1, g1, b1] = [0, x, chroma]; else if (hue1 >= 4 && hue1 <= 5) [r1, g1, b1] = [x, 0, chroma]; else if (hue1 >= 5 && hue1 <= 6) [r1, g1, b1] = [chroma, 0, x]; else [r1, g1, b1] = [0, 0, 0]; return new Color((r1 + m) * 255, (g1 + m) * 255, (b1 + m) * 255, alpha); } /** * Parses a hex rgba string to a color object. It also accepts values without * the alpha component. * @param value the string which should be parsed. */ public static fromHexRgbA(value: string): Color { if (value.length === 7) value = value.padEnd(9, "FF"); if (!Color.HEX_RGBA_REGEX.test(value)) throw new Error(`Opaque hex-rgb string "${value}" is invalid`); const red = Color.parseHexColorComponent(value.slice(1, 3)); const green = Color.parseHexColorComponent(value.slice(3, 5)); const blue = Color.parseHexColorComponent(value.slice(5, 7)); const alpha = Color.parseHexColorComponent(value.slice(7, 9)); return new Color(red, green, blue, alpha); } /** * Checks if the provided HSV color is valid * @param hue value from `0` to `360` * @param saturation value from `0` to `1` * @param value value from `0` to `1` */ public static isValidHSV( hue: number, saturation: number, value: number, ): boolean { return ( hue >= 0 && hue <= 360 && saturation >= 0 && saturation <= 1 && value >= 0 && value <= 1 ); } /** * Parses a color component into it's integer representation. * @param component Component to parse, eg `0F` or `00` or `12`. * @param which Optional parameter to enrich the error message with the * information of what component actually failed to parse. */ public static parseHexColorComponent( component: string, which?: string, ): number { try { return parseInt(component, 16); } catch { let msg = `Failed to parse hex color comp "${component}".`; if (which !== undefined) msg += ` Error occurred whilst parsing "${which}".`; throw new Error(msg); } } /** * Generates a random color. Note that the opacity is **never** randomizes, * thus always opaque. * @returns A random color. */ public static get random(): Color { const random = () => Math.floor(Math.random() * 256); return new Color(random(), random(), random(), 255); } private _red; private _green; private _blue; private _alpha; /** * Generates a new color object using RGBA * @param red Red component of the value `0` to `255` * @param green Green component of the value `0` to `255` * @param blue Blue component of the value `0` to `255` * @param alpha Alpha component of the value `0` to `255` */ public constructor(red: number, green: number, blue: number, alpha: number) { this._red = red; this._green = green; this._blue = blue; this._alpha = alpha; } public get clone(): Color { return new Color(...this.rgba); } public get red(): number { return this._red; } public set red(val: number) { this._red = val; } public setRed(val: number): Color { this._red = val; return this; } public get green(): number { return this._green; } public set green(val: number) { this._green = val; } public setGreen(val: number): Color { this._green = val; return this; } public get blue(): number { return this._blue; } public set blue(val: number) { this._blue = val; } public setBlue(val: number): Color { this._blue = val; return this; } public get alpha(): number { return this._alpha; } public set alpha(val: number) { this._alpha = val; } public setAlpha(val: number): Color { this._alpha = val; return this; } /** * Returns the hsva (hue, saturation, value, alpha) values of the color. * * @returns HSVA values. Note that the alpha values are normalized (`0` to * `1`), not `0` to `255`. */ public get hsva(): [ hue: number, saturation: number, value: number, alpha: number, ] { const r = this._red / 255; const g = this._green / 255; const b = this._blue / 255; const a = this._alpha / 255; const compMax = Math.max(r, g, b); const compMin = Math.min(r, g, b); const delta = compMax - compMin; let h: number; if (delta === 0) h = 0; else { switch (compMax) { default: throw new Error(`Invalid compMax: "${compMax}"`); case r: h = (g - b) / delta; break; case g: h = (b - r) / delta + 2; break; case b: h = (r - g) / delta + 4; break; } } const v = compMax; const s = v === 0 ? 0 : delta / v; const clampedH = h % 6; const normalizedH = clampedH < 0 ? clampedH + 6 : clampedH; return [normalizedH * 60, s, v, a]; } /** * @returns the RGBA representation of the color. * * Example: ```const [red, green, blue, alpha] = color.toRGBA()``` */ public get rgba(): [number, number, number, number] { return [this._red, this._green, this._blue, this._alpha]; } /** * Returns the hex string representation ignoring alpha. For example `red` * would be `#FF0000`. * @returns Hex string color representation ignoring alpha. */ public get hexRgb(): string { const { _red, _green, _blue, toHexDigit } = this; return "#" + [_red, _green, _blue].map(toHexDigit).join(""); } /** * Returns the hex string representation including alpha. For example `red`, * 50% transparent would be `#FF00007F` * @returns Hex string color representation including alpha. */ public get hexRgba(): string { const { _red, _green, _blue, _alpha, toHexDigit } = this; return "#" + [_red, _green, _blue, _alpha].map(toHexDigit).join(""); } public get isDark(): boolean { // "average" const weightedAverage = Math.sqrt( 0.299 * this._red * this._red + 0.587 * this._green * this._green + 0.114 * this._blue * this._blue, ); return weightedAverage < 127.5; } /** * Takes a number between `0` and `255` and converts it to hexadecimal. * @param n number between `0` and `255` */ private toHexDigit(n: number): string { return Math.trunc(n).toString(16).padStart(2, "0"); } }