From aa2ba2c3749e86404663051ee5b62c7988632652 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Tue, 24 Nov 2020 14:47:07 +0100 Subject: [PATCH] feat: color methods Should fill most needs and remove the need for external libs like tinycolor --- packages/core/color/color-common.ts | 291 ++++++++++++++++++++++++++++ packages/core/color/index.d.ts | 115 +++++++++++ 2 files changed, 406 insertions(+) diff --git a/packages/core/color/color-common.ts b/packages/core/color/color-common.ts index 6c6bd72e9..8408838f3 100644 --- a/packages/core/color/color-common.ts +++ b/packages/core/color/color-common.ts @@ -37,6 +37,11 @@ export class Color implements definition.Color { // The parameter is a 32-bit unsigned integer where each 8 bits specify a color component // In case a 32-bit signed int (Android, Java has no unsigned types) was provided - convert to unsigned by applyint >>> 0 this._argb = arg >>> 0; + } else if (arg && arg._argb) { + // we would go there if a color was passed as an argument (or an object which is why we dont do instanceof) + // The parameter is a 32-bit unsigned integer where each 8 bits specify a color component + // In case a 32-bit signed int (Android, Java has no unsigned types) was provided - convert to unsigned by applyint >>> 0 + this._argb = arg._argb >>> 0; } else { throw new Error('Expected 1 or 4 constructor parameters.'); } @@ -137,6 +142,10 @@ export class Color implements definition.Color { return HEX_REGEX.test(value) || isRgbOrRgba(value) || isHslOrHsla(value); } + public static fromHSL(a, h, s, l) { + const rgb = hslToRgb(h, s, l); + return new Color(a, rgb.r, rgb.g, rgb.b); + } private _componentToHex(component: number): string { let hex = component.toString(16); @@ -163,6 +172,205 @@ export class Color implements definition.Color { public static fromIosColor(value: UIColor): Color { return undefined; } + + /** + * return true if brightenss < 128 + * + */ + public isDark() { + return this.getBrightness() < 128; + } + + /** + * return true if brightenss >= 128 + * + */ + public isLight() { + return !this.isDark(); + } + + /** + * return the [brightness](http://www.w3.org/TR/AERT#color-contrast) + * + */ + public getBrightness() { + return (this.r * 299 + this.g * 587 + this.b * 114) / 1000; + } + + /** + * return the [luminance](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef) + * + */ + public getLuminance() { + let R, G, B; + const RsRGB = this.r / 255; + const GsRGB = this.g/255; + const BsRGB = this.b/255; + + if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);} + if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);} + if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);} + return (0.2126 * R) + (0.7152 * G) + (0.0722 * B); + } + + /** + * Return this color (as a new Color instance) with the provided alpha + * + * @param alpha (between 0 and 255) + */ + public setAlpha(a: number) { + return new Color(a, this.r, this.g, this.b); + } + + /** + * return the hsl representation of the color + * + */ + public toHsl() { + const hsl = rgbToHsl(this.r, this.g, this.b); + return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this.a }; + } + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_hsl.asp) representation of the color + * + */ + public toHslString() { + const hsl = rgbToHsl(this.r, this.g, this.b); + const h = Math.round(hsl.h * 360), s = Math.round(hsl.s * 100), l = Math.round(hsl.l * 100); + const a = this.a; + return (a == 255) ? + "hsl(" + h + ", " + s + "%, " + l + "%)" : + "hsla(" + h + ", " + s + "%, " + l + "%, "+ (a/255).toFixed(2) + ")"; + } + + /** + * return the hsv representation of the color + * + */ + public toHsv() { + const hsv = rgbToHsv(this.r, this.g, this.b); + return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this.a }; + } + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toHsvString() { + const hsv = rgbToHsv(this.r, this.g, this.b); + const h = Math.round(hsv.h * 360), s = Math.round(hsv.s * 100), v = Math.round(hsv.v * 100); + const a = this.a; + return (a == 255) ? + "hsv(" + h + ", " + s + "%, " + v + "%)" : + "hsva(" + h + ", " + s + "%, " + v + "%, "+ (a/255).toFixed(2) + ")"; + } + + /** + * return the [CSS rgb](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toRgbString() { + const a = this.a; + return (a == 1) ? + "rgb(" + Math.round(this.r) + ", " + Math.round(this.g) + ", " + Math.round(this.b) + ")" : + "rgba(" + Math.round(this.r) + ", " + Math.round(this.g) + ", " + Math.round(this.b) + ", " + (a/255).toFixed(2) + ")"; + } + + /** + * Desaturate the color a given amount, from 0 to 100. Providing 100 will is the same as calling greyscale. + * + * @param amount (between 0 and 100) + */ + public desaturate(amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.s -= amount / 100; + hsl.s = Math.min(1, Math.max(0, hsl.s)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Saturate the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public saturate(amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.s += amount / 100; + hsl.s = Math.min(1, Math.max(0, hsl.s)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Completely desaturates a color into greyscale. Same as calling desaturate(100). + * + */ + public greyscale() { + return this.desaturate(100); + } + + /** + * Lighten the color a given amount, from 0 to 100. Providing 100 will always return white. + * + * @param amount (between 0 and 100) + */ + public lighten (amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.l += amount / 100; + hsl.l = Math.min(1, Math.max(0, hsl.l)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Brighten the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public brighten(amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const r = Math.max(0, Math.min(255, this.r - Math.round(255 * - (amount / 100)))); + const g = Math.max(0, Math.min(255, this.g - Math.round(255 * - (amount / 100)))); + const b = Math.max(0, Math.min(255, this.b - Math.round(255 * - (amount / 100)))); + return new Color(this.a, r, g, b); + } + + /** + * Darken the color a given amount, from 0 to 100. Providing 100 will always return black. + * + * @param amount (between 0 and 100) + */ + public darken (amount: number) { + amount = (amount === 0) ? 0 : (amount || 10); + const hsl = this.toHsl(); + hsl.l -= amount / 100; + hsl.l = Math.min(1, Math.max(0, hsl.l)) + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * Spin the hue a given amount, from -360 to 360. Calling with 0, 360, or -360 will do nothing (since it sets the hue back to what it was before). + * + * @param amount (between 0 and 100) + */ + public spin(amount: number) { + const hsl = this.toHsl(); + const hue = (hsl.h + amount) % 360; + hsl.h = hue < 0 ? 360 + hue : hue; + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } + + /** + * returns the color complement + * + */ + public complement() { + const hsl = this.toHsl(); + hsl.h = (hsl.h + 180) % 360; + return Color.fromHSL(this.a, hsl.h, hsl.s, hsl.l); + } } function isRgbOrRgba(value: string): boolean { @@ -222,3 +430,86 @@ function argbFromHslOrHsla(value: string): number { return (a & 0xff) * 0x01000000 + (r & 0xff) * 0x00010000 + (g & 0xff) * 0x00000100 + (b & 0xff); } + +// `rgbToHsl` +// Converts an RGB color value to HSL. +// *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] +// *Returns:* { h, s, l } in [0,1] +function rgbToHsl(r, g, b) { + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s; + const l = (max + min) / 2; + + if(max == min) { + h = s = 0; // achromatic + } + else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + + h /= 6; + } + + return { h: h, s: s, l: l }; +} + +function hue2rgb(p, q, t) { + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1/6) return p + (q - p) * 6 * t; + if(t < 1/2) return q; + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; +} + +// `hslToRgb` +// Converts an HSL color value to RGB. +// *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] +// *Returns:* { r, g, b } in the set [0, 255] +function hslToRgb(h, s, l) { + let r, g, b; + if(s === 0) { + r = g = b = l; // achromatic + } + else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return { r: r * 255, g: g * 255, b: b * 255 }; +} + + +// `rgbToHsv` +// Converts an RGB color value to HSV +// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] +// *Returns:* { h, s, v } in [0,1] +function rgbToHsv(r, g, b) { + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h; + const v = max; + + const d = max - min; + const s = max === 0 ? 0 : d / max; + + if(max == min) { + h = 0; // achromatic + } + else { + switch(max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h, s: s, v: v }; +} diff --git a/packages/core/color/index.d.ts b/packages/core/color/index.d.ts index e6529fb21..56ac39610 100644 --- a/packages/core/color/index.d.ts +++ b/packages/core/color/index.d.ts @@ -75,4 +75,119 @@ export class Color { * Creates color from iOS-specific UIColor value representation. */ public static fromIosColor(value: any /* UIColor */): Color; + + /** + * return true if brightenss < 128 + * + */ + public isDark(): boolean; + + /** + * return true if brightenss >= 128 + * + */ + public isLight(): boolean; + + /** + * return the [brightness](http://www.w3.org/TR/AERT#color-contrast) + * + */ + public getBrightness(): number; + /** + * return the [luminance](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef) + * + */ + public getLuminance(): number; + + /** + * Return this color (as a new Color instance) with the provided alpha + * + * @param alpha (between 0 and 255) + */ + public setAlpha(a: number): Color; + /** + * return the hsl representation of the color + * + */ + public toHsl() : { h: number, s: number, l: number, a: number }; + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_hsl.asp) representation of the color + * + */ + public toHslString(): string; + + /** + * return the hsv representation of the color + * + */ + public toHsv(): { h: number, s: number, v: number, a: number }; + + /** + * return the [CSS hsv](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toHsvString(): string; + + /** + * return the [CSS rgb](https://www.w3schools.com/Css/css_colors_rgb.asp) representation of the color + * + */ + public toRgbString(): string; + + /** + * Desaturate the color a given amount, from 0 to 100. Providing 100 will is the same as calling greyscale. + * + * @param amount (between 0 and 100) + */ + public desaturate(amount: number): Color; + + /** + * Saturate the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public saturate(amount: number): Color; + + /** + * Completely desaturates a color into greyscale. Same as calling desaturate(100). + * + * @returns + */ + public greyscale(): Color; + + /** + * Lighten the color a given amount, from 0 to 100. Providing 100 will always return white. + * + * @param amount (between 0 and 100) + * @returns olor : Color + */ + public lighten (amount: number): Color; + + /** + * Brighten the color a given amount, from 0 to 100. + * + * @param amount (between 0 and 100) + */ + public brighten(amount: number): Color; + + /** + * Darken the color a given amount, from 0 to 100. Providing 100 will always return black. + * + * @param amount (between 0 and 100) + */ + public darken (amount: number): Color; + + /** + * Spin the hue a given amount, from -360 to 360. Calling with 0, 360, or -360 will do nothing (since it sets the hue back to what it was before). + * + * @param amount (between 0 and 100) + */ + public spin(amount: number): Color; + + /** + * returns the color complement + * + */ + public complement(): Color; }