diff --git a/tns-core-modules/color/color-common.ts b/tns-core-modules/color/color-common.ts index b0b1492537..b33e63e638 100644 --- a/tns-core-modules/color/color-common.ts +++ b/tns-core-modules/color/color-common.ts @@ -1,6 +1,7 @@ -import * as definition from "."; +import * as definition from "."; import * as types from "../utils/types"; import * as knownColors from "./known-colors"; +import { convertHSLToRGBColor } from "tns-core-modules/css/parser"; const SHARP = "#"; const HEX_REGEX = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)|(^#[0-9A-F]{8}$)/i; @@ -18,6 +19,8 @@ export class Color implements definition.Color { if (types.isString(arg)) { if (isRgbOrRgba(arg)) { this._argb = argbFromRgbOrRgba(arg); + } else if (isHslOrHsla(arg)) { + this._argb = argbFromHslOrHsla(arg); } else if (knownColors.isKnownName(arg)) { // The parameter is a known color name const hex = knownColors.getKnownColor(arg); @@ -127,7 +130,7 @@ export class Color implements definition.Color { return true; } - return HEX_REGEX.test(value) || isRgbOrRgba(value); + return HEX_REGEX.test(value) || isRgbOrRgba(value) || isHslOrHsla(value); } private _componentToHex(component: number): string { @@ -162,33 +165,58 @@ function isRgbOrRgba(value: string): boolean { return (toLower.indexOf("rgb(") === 0 || toLower.indexOf("rgba(") === 0) && toLower.indexOf(")") === (toLower.length - 1); } -function argbFromRgbOrRgba(value: string): number { +function isHslOrHsla(value: string): boolean { + const toLower = value.toLowerCase(); + + return (toLower.indexOf("hsl(") === 0 || toLower.indexOf("hsla(") === 0) && toLower.indexOf(")") === (toLower.length - 1); +} + +function parseColorWithAlpha(value: string): any { const toLower = value.toLowerCase(); - const parts = toLower.replace("rgba(", "").replace("rgb(", "").replace(")", "").trim().split(","); + const parts = toLower.replace(/(rgb|hsl)a?\(/, "") + .replace(")", "") + .trim().split(","); - let r = 255; - let g = 255; - let b = 255; + let f = 255; + let s = 255; + let t = 255; let a = 255; if (parts[0]) { - r = parseInt(parts[0].trim()); + f = parseInt(parts[0].trim()); } if (parts[1]) { - g = parseInt(parts[1].trim()); + s = parseInt(parts[1].trim()); } if (parts[2]) { - b = parseInt(parts[2].trim()); + t = parseInt(parts[2].trim()); } if (parts[3]) { a = Math.round(parseFloat(parts[3].trim()) * 255); } + return { f, s, t, a }; +} + +function argbFromRgbOrRgba(value: string): number { + const { f: r, s: g, t: b, a } = parseColorWithAlpha(value); + + return (a & 0xFF) * 0x01000000 + + (r & 0xFF) * 0x00010000 + + (g & 0xFF) * 0x00000100 + + (b & 0xFF); +} + +function argbFromHslOrHsla(value: string): number { + const { f: h, s: s, t: l, a } = parseColorWithAlpha(value); + + const { r, g, b } = convertHSLToRGBColor(h, s, l); + return (a & 0xFF) * 0x01000000 + (r & 0xFF) * 0x00010000 + (g & 0xFF) * 0x00000100 - + (b & 0xFF) * 0x00000001; + + (b & 0xFF); } diff --git a/tns-core-modules/css/parser.ts b/tns-core-modules/css/parser.ts index 28d464209c..1ab2a7823c 100644 --- a/tns-core-modules/css/parser.ts +++ b/tns-core-modules/css/parser.ts @@ -84,7 +84,7 @@ export function parseHexColor(text: string, start: number = 0): Parsed { function rgbaToArgbNumber(r: number, g: number, b: number, a: number = 1): number | undefined { if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255 && a >= 0 && a <= 1) { - return (Math.round(a * 0xFF) * 0x01000000) + (r * 0x010000) + (g * 0x000100) + (b * 0x000001); + return (Math.round(a * 0xFF) * 0x01000000) + (r * 0x010000) + (g * 0x000100) + b; } else { return null; } @@ -116,6 +116,67 @@ export function parseRGBAColor(text: string, start: number = 0): Parsed { return { start, end, value }; } +export function convertHSLToRGBColor(hue: number, saturation: number, lightness: number): { r: number; g: number; b: number; } { + // Per formula it will be easier if hue is divided to 60° and saturation to 100 beforehand + // https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB + hue /= 60; + lightness /= 100; + + let chroma = (1 - Math.abs(2 * lightness - 1)) * saturation / 100, + X = chroma * (1 - Math.abs(hue % 2 - 1)), + // Add lightness match to all RGB components beforehand + { m: r, m: g, m: b } = { m: lightness - chroma / 2 }; + + if (0 <= hue && hue < 1) { r += chroma; g += X; } + else if (hue < 2) { r += X; g += chroma; } + else if (hue < 3) { g += chroma; b += X; } + else if (hue < 4) { g += X; b += chroma; } + else if (hue < 5) { r += X; b += chroma; } + else if (hue < 6) { r += chroma; b += X; } + + return { + r: Math.round(r * 0xFF), + g: Math.round(g * 0xFF), + b: Math.round(b * 0xFF) + }; +} + +function hslaToArgbNumber(h: number, s: number, l: number, a: number = 1): number | undefined { + let { r, g, b } = convertHSLToRGBColor(h, s, l); + + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255 && a >= 0 && a <= 1) { + return (Math.round(a * 0xFF) * 0x01000000) + (r * 0x010000) + (g * 0x000100) + b; + } else { + return null; + } +} + +const hslColorRegEx = /\s*(hsl\(\s*([\d.]*)\s*,\s*([\d.]*)%\s*,\s*([\d.]*)%\s*\))/gy; +export function parseHSLColor(text: string, start: number = 0): Parsed { + hslColorRegEx.lastIndex = start; + const result = hslColorRegEx.exec(text); + if (!result) { + return null; + } + const end = hslColorRegEx.lastIndex; + const value = result[1] && hslaToArgbNumber(parseFloat(result[2]), parseFloat(result[3]), parseFloat(result[4])); + + return { start, end, value }; +} + +const hslaColorRegEx = /\s*(hsla\(\s*([\d.]*)\s*,\s*([\d.]*)%\s*,\s*([\d.]*)%\s*,\s*([01]?\.?\d*)\s*\))/gy; +export function parseHSLAColor(text: string, start: number = 0): Parsed { + hslaColorRegEx.lastIndex = start; + const result = hslaColorRegEx.exec(text); + if (!result) { + return null; + } + const end = hslaColorRegEx.lastIndex; + const value = hslaToArgbNumber(parseFloat(result[2]), parseFloat(result[3]), parseFloat(result[4]), parseFloat(result[5])); + + return { start, end, value }; +} + export enum colors { transparent = 0x00000000, aliceblue = 0xFFF0F8FF, @@ -280,7 +341,12 @@ export function parseColorKeyword(value, start: number, keyword = parseKeyword(v } export function parseColor(value: string, start: number = 0, keyword = parseKeyword(value, start)): Parsed { - return parseHexColor(value, start) || parseColorKeyword(value, start, keyword) || parseRGBColor(value, start) || parseRGBAColor(value, start); + return parseHexColor(value, start) || + parseColorKeyword(value, start, keyword) || + parseRGBColor(value, start) || + parseRGBAColor(value, start) || + parseHSLColor(value, start) || + parseHSLAColor(value, start); } const keywordRegEx = /\s*([a-z][\w\-]*)\s*/giy; diff --git a/unit-tests/css/parser.ts b/unit-tests/css/parser.ts index 667f12177a..fa21ed04de 100644 --- a/unit-tests/css/parser.ts +++ b/unit-tests/css/parser.ts @@ -56,6 +56,8 @@ describe("css", () => { test(parseColor, " #85456789 ", { start: 0, end: 12, value: 0x85456789 }); test(parseColor, " rgb(255, 8, 128) ", { start: 0, end: 18, value: 0xFFFF0880 }); test(parseColor, " rgba(255, 8, 128, 0.5) ", { start: 0, end: 24, value: 0x80FF0880 }); + test(parseColor, " hsl(330.9, 100%, 51.6%) ", { start: 0, end: 25, value: 0xFFFF0880 }); + test(parseColor, " hsla(330.9, 100%, 51.6%, 0.5) ", { start: 0, end: 31, value: 0x80FF0880 }); test(parseColor, "#FF0000 url(lucky.gif)", 8, null); test(parseColor, "url(lucky.gif) #FF0000 repeat", 15, { start: 15, end: 23, value: 0xFFFF0000 }); }); @@ -175,11 +177,11 @@ describe("css", () => { test(parseSelector, `[src ${attributeTest} "val"]`, { start: 0, end: 12 + attributeTest.length, value: [[[{ type: "[]", property: "src", test: attributeTest, value: "val"}], undefined]]}); }); test(parseSelector, "listview > .image", { start: 0, end: 17, value: [ - [[{ type: "", identifier: "listview"}], ">"], + [[{ type: "", identifier: "listview"}], ">"], [[{ type: ".", identifier: "image"}], undefined] ]}); test(parseSelector, "listview .image", { start: 0, end: 16, value: [ - [[{ type: "", identifier: "listview"}], " "], + [[{ type: "", identifier: "listview"}], " "], [[{ type: ".", identifier: "image"}], undefined] ]}); test(parseSelector, "button:hover", { start: 0, end: 12, value: [[[{ type: "", identifier: "button" }, { type: ":", identifier: "hover"}], undefined]]});