feat(css): color-mix (#10719)

This commit is contained in:
Nathan Walker
2025-03-16 15:48:40 -07:00
committed by GitHub
parent e2f9687e72
commit cfc27ebb90
11 changed files with 499 additions and 266 deletions

View File

@ -1708,18 +1708,18 @@ export function test_CascadingClassNamesAppliesAfterPageLoad() {
export function test_evaluateCssCalcExpression() { export function test_evaluateCssCalcExpression() {
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(1px + 1px)'), '2px', 'Simple calc (1)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(1px + 1px)'), '2px', 'Simple calc (1)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(50px - (20px - 30px))'), '60px', 'Simple calc (2)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(50px - (20px - 30px))'), '60px', 'Simple calc (2)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100px - (100px - 100%))'), '100%', 'Simple calc (3)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100px - (100px - 100%))'), 'calc(100px - (100px - 100%))', 'Simple calc (3)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100px + (100px - 100%))'), 'calc(200px - 100%)', 'Simple calc (4)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100px + (100px - 100%))'), 'calc(100px + (100px - 100%))', 'Simple calc (4)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100% - 10px + 20px)'), 'calc(100% + 10px)', 'Simple calc (5)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100% - 10px + 20px)'), 'calc(100% - 10px + 20px)', 'Simple calc (5)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100% + 10px - 20px)'), 'calc(100% - 10px)', 'Simple calc (6)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(100% + 10px - 20px)'), 'calc(100% + 10px - 20px)', 'Simple calc (6)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(10.px + .0px)'), '10px', 'Simple calc (8)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(10.px + .0px)'), 'calc(10.px + .0px)', 'Simple calc (8)');
TKUnit.assertEqual(_evaluateCssCalcExpression('a calc(1px + 1px)'), 'a 2px', 'Ignore value surrounding calc function (1)'); TKUnit.assertEqual(_evaluateCssCalcExpression('a calc(1px + 1px)'), 'a 2px', 'Ignore value surrounding calc function (1)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(1px + 1px) a'), '2px a', 'Ignore value surrounding calc function (2)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(1px + 1px) a'), '2px a', 'Ignore value surrounding calc function (2)');
TKUnit.assertEqual(_evaluateCssCalcExpression('a calc(1px + 1px) b'), 'a 2px b', 'Ignore value surrounding calc function (3)'); TKUnit.assertEqual(_evaluateCssCalcExpression('a calc(1px + 1px) b'), 'a 2px b', 'Ignore value surrounding calc function (3)');
TKUnit.assertEqual(_evaluateCssCalcExpression('a calc(1px + 1px) b calc(1em + 2em) c'), 'a 2px b 3em c', 'Ignore value surrounding calc function (4)'); TKUnit.assertEqual(_evaluateCssCalcExpression('a calc(1px + 1px) b calc(1em + 2em) c'), 'a 2px b 3em c', 'Ignore value surrounding calc function (4)');
TKUnit.assertEqual(_evaluateCssCalcExpression(`calc(\n1px \n* 2 \n* 1.5)`), '3px', 'Handle new lines'); TKUnit.assertEqual(_evaluateCssCalcExpression(`calc(\n1px \n* 2 \n* 1.5)`), '3px', 'Handle new lines');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(1/100)'), '0.01', 'Handle precision correctly (1)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(1/100)'), '0.01', 'Handle precision correctly (1)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(5/1000000)'), '0.00001', 'Handle precision correctly (2)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(5/1000000)'), '0.000005', 'Handle precision correctly (2)');
TKUnit.assertEqual(_evaluateCssCalcExpression('calc(5/100000)'), '0.00005', 'Handle precision correctly (3)'); TKUnit.assertEqual(_evaluateCssCalcExpression('calc(5/100000)'), '0.00005', 'Handle precision correctly (3)');
} }
@ -1826,6 +1826,37 @@ export function test_nested_css_calc() {
TKUnit.assertDeepEqual(stack.width, { unit: '%', value: 0.5 }, 'Stack - width === 50%'); TKUnit.assertDeepEqual(stack.width, { unit: '%', value: 0.5 }, 'Stack - width === 50%');
} }
export function test_evaluateCssColorMixExpression() {
TKUnit.assertEqual(new Color('color-mix(in lch longer hue, hsl(200deg 50% 80%), coral)').toRgbString(), 'rgba(136, 202, 134, 1.00)', 'Color mix (1)');
TKUnit.assertEqual(new Color('color-mix(in hsl, hsl(200 50 80), coral 80%)').toRgbString(), 'rgba(247, 103, 149, 1.00)', 'Color mix (2)');
TKUnit.assertEqual(new Color('color-mix(in srgb, plum, #f00)').toRgbString(), 'rgba(238, 80, 110, 1.00)', 'Color mix (4)');
TKUnit.assertEqual(new Color('color-mix(in lab, plum 60%, #f00 50%)').toRgbString(), 'rgba(247, 112, 125, 1.00)', 'Color mix (5)');
TKUnit.assertEqual(new Color('color-mix(in --swop5c, red, blue)').toRgbString(), 'rgba(0, 0, 255, 0.00)', 'Color mix (6)');
}
export function test_nested_css_color_mix() {
const page = helper.getClearCurrentPage();
const stack = new StackLayout();
stack.css = `
StackLayout.coral {
background-color: color-mix(in hsl, hsl(200 50 80), coral 80%);
}
`;
const label = new Label();
page.content = stack;
stack.addChild(label);
stack.className = 'coral';
TKUnit.assertEqual((stack.backgroundColor as Color).toRgbString(), 'rgba(247, 103, 149, 1.00)', 'Stack - backgroundColor === color-mix(in hsl, hsl(200 50 80), coral 80%)');
(stack as any).style = `background-color: color-mix(in --swop5c, red, blue);`;
TKUnit.assertDeepEqual((stack.backgroundColor as Color).toRgbString(), 'rgba(0, 0, 255, 0.00)', 'Stack - backgroundColor === color-mix(in --swop5c, red, blue)');
}
export function test_css_variables() { export function test_css_variables() {
const blackColor = '#000000'; const blackColor = '#000000';
const redColor = '#FF0000'; const redColor = '#FF0000';

View File

@ -1,6 +1,11 @@
@import 'nativescript-theme-core/css/core.light.css'; @import 'nativescript-theme-core/css/core.light.css';
@import './_app-platform.css'; @import './_app-platform.css';
/** define shared global variables to test in toolbox */
* {
--color-black: black;
}
/* /*
The following CSS rule changes the font size of all UI The following CSS rule changes the font size of all UI
components that have the btn class name. components that have the btn class name.
@ -8,6 +13,20 @@ components that have the btn class name.
Button { Button {
text-transform: none; text-transform: none;
} }
.calc-padding {
padding: calc(4 * 6);
}
.colormix {
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
/* background-color: color-mix(in lch longer hue, hsl(200deg 50% 80%), coral); */
/* background-color: color-mix(in hsl, hsl(200 50 80), coral 80%); */
/* background-color: color-mix(in srgb, plum, #f00); */
/* background-color: color-mix(in lab, plum 60%, #f00 50%); */
/* background-color: color-mix(in --swop5c, red, blue); */
}
.btn-view-demo { .btn-view-demo {
/* background-color: #65ADF1; */ /* background-color: #65ADF1; */
border-radius: 8; border-radius: 8;

View File

@ -5,6 +5,7 @@
</Page.actionBar> </Page.actionBar>
<StackLayout> <StackLayout>
<ScrollView class="h-full"> <ScrollView class="h-full">
<!-- experiment with calc and colormix with classes: calc-padding colormix -->
<StackLayout class="p-20" paddingBottom="40" iosOverflowSafeArea="false"> <StackLayout class="p-20" paddingBottom="40" iosOverflowSafeArea="false">
<Button text="a11y" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" /> <Button text="a11y" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="box-shadow" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" /> <Button text="box-shadow" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />

145
package-lock.json generated
View File

@ -13,6 +13,10 @@
"nativescript-theme-core": "^1.0.4" "nativescript-theme-core": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@csstools/css-calc": "~2.1.2",
"@csstools/css-color-parser": "^3.0.8",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@nativescript/hook": "^2.0.0", "@nativescript/hook": "^2.0.0",
"@nativescript/nx": "^20.0.0", "@nativescript/nx": "^20.0.0",
"@nstudio/focus": "^20.0.2", "@nstudio/focus": "^20.0.2",
@ -59,7 +63,6 @@
"parse-css": "git+https://github.com/tabatkins/parse-css.git", "parse-css": "git+https://github.com/tabatkins/parse-css.git",
"parserlib": "^1.1.1", "parserlib": "^1.1.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"reduce-css-calc": "~2.1.7",
"sass": "^1.72.0", "sass": "^1.72.0",
"shady-css-parser": "^0.1.0", "shady-css-parser": "^0.1.0",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
@ -2129,6 +2132,121 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz",
"integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz",
"integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.0.2",
"@csstools/css-calc": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
"integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
"integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@ -11063,13 +11181,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/css-unit-converter": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
"integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
"dev": true,
"license": "MIT"
},
"node_modules/css-what": { "node_modules/css-what": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
@ -20966,13 +21077,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prefix-matches": { "node_modules/prefix-matches": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prefix-matches/-/prefix-matches-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prefix-matches/-/prefix-matches-1.0.1.tgz",
@ -21592,17 +21696,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/reduce-css-calc": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
"integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"css-unit-converter": "^1.1.1",
"postcss-value-parser": "^3.3.0"
}
},
"node_modules/regenerate": { "node_modules/regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",

View File

@ -20,6 +20,10 @@
"nativescript-theme-core": "^1.0.4" "nativescript-theme-core": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@csstools/css-calc": "~2.1.2",
"@csstools/css-color-parser": "^3.0.8",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@nativescript/hook": "^2.0.0", "@nativescript/hook": "^2.0.0",
"@nativescript/nx": "^20.0.0", "@nativescript/nx": "^20.0.0",
"@nstudio/focus": "^20.0.2", "@nstudio/focus": "^20.0.2",
@ -66,7 +70,6 @@
"parse-css": "git+https://github.com/tabatkins/parse-css.git", "parse-css": "git+https://github.com/tabatkins/parse-css.git",
"parserlib": "^1.1.1", "parserlib": "^1.1.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"reduce-css-calc": "~2.1.7",
"sass": "^1.72.0", "sass": "^1.72.0",
"shady-css-parser": "^0.1.0", "shady-css-parser": "^0.1.0",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
@ -85,4 +88,4 @@
"npx prettier --write" "npx prettier --write"
] ]
} }
} }

View File

@ -2,9 +2,7 @@ import * as definition from '.';
import * as types from '../utils/types'; import * as types from '../utils/types';
import * as knownColors from './known-colors'; import * as knownColors from './known-colors';
import { Color } from '.'; import { Color } from '.';
import { HEX_REGEX, argbFromColorMix, argbFromHslOrHsla, argbFromHsvOrHsva, argbFromRgbOrRgba, hslToRgb, hsvToRgb, isCssColorMixExpression, isHslOrHsla, isHsvOrHsva, isRgbOrRgba, rgbToHsl, rgbToHsv, argbFromString } from './color-utils';
const SHARP = '#';
const HEX_REGEX = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)|(^#[0-9A-F]{8}$)|(^#[0-9A-F]{4}$)/i;
export class ColorBase implements definition.Color { export class ColorBase implements definition.Color {
private _argb: number; private _argb: number;
@ -29,7 +27,9 @@ export class ColorBase implements definition.Color {
const argb = knownColors.getKnownColor(lowered); const argb = knownColors.getKnownColor(lowered);
this._name = arg; this._name = arg;
this._argb = argb; this._argb = argb;
} else if (arg[0].charAt(0) === SHARP && (arg.length === 4 || arg.length === 5 || arg.length === 7 || arg.length === 9)) { } else if (isCssColorMixExpression(lowered)) {
this._argb = argbFromColorMix(lowered);
} else if (arg[0].charAt(0) === '#' && (arg.length === 4 || arg.length === 5 || arg.length === 7 || arg.length === 9)) {
// we dont use the regexp as it is quite slow. Instead we expect it to be a valid hex format // we dont use the regexp as it is quite slow. Instead we expect it to be a valid hex format
// strange that it would not be. And if it is not a thrown error seems best // strange that it would not be. And if it is not a thrown error seems best
// The parameter is a "#RRGGBBAA" formatted string // The parameter is a "#RRGGBBAA" formatted string
@ -89,7 +89,7 @@ export class ColorBase implements definition.Color {
} }
get hex(): string { get hex(): string {
let result = SHARP + ('000000' + (this._argb & 0xffffff).toString(16)).toUpperCase().slice(-6); let result = '#' + ('000000' + (this._argb & 0xffffff).toString(16)).toUpperCase().slice(-6);
if (this.a !== 0xff) { if (this.a !== 0xff) {
return (result += ('00' + this.a.toString(16).toUpperCase()).slice(-2)); return (result += ('00' + this.a.toString(16).toUpperCase()).slice(-2));
} }
@ -109,28 +109,7 @@ export class ColorBase implements definition.Color {
} }
public _argbFromString(hex: string): number { public _argbFromString(hex: string): number {
// always called as SHARP as first char return argbFromString(hex);
hex = hex.substring(1);
const length = hex.length;
// first we normalize
if (length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
} else if (length === 4) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
}
let intVal = parseInt(hex, 16);
if (hex.length === 6) {
// add the alpha component since the provided string is RRGGBB
intVal = (intVal & 0x00ffffff) + 0xff000000;
} else {
// the new format is #RRGGBBAA
// we need to shift the alpha value to 0x01000000 position
const a = (intVal / 0x00000001) & 0xff;
intVal = (intVal >>> 8) + (a & 0xff) * 0x01000000;
}
return intVal;
} }
public equals(value: definition.Color): boolean { public equals(value: definition.Color): boolean {
@ -397,196 +376,3 @@ export class ColorBase implements definition.Color {
return new Color(rgba.a, rgba.r, rgba.g, rgba.b); return new Color(rgba.a, rgba.r, rgba.g, rgba.b);
} }
} }
function isRgbOrRgba(value: string): boolean {
return (value.startsWith('rgb(') || value.startsWith('rgba(')) && value.endsWith(')');
}
function isHslOrHsla(value: string): boolean {
return (value.startsWith('hsl') || value.startsWith('hsla(')) && value.endsWith(')');
}
function isHsvOrHsva(value: string): boolean {
return (value.startsWith('hsv') || value.startsWith('hsva(')) && value.endsWith(')');
}
function parseColorWithAlpha(value: string): any {
const separator = value.indexOf(',') !== -1 ? ',' : ' ';
const parts = value
.replace(/(rgb|hsl|hsv)a?\(/, '')
.replace(')', '')
.replace(/\//, ' ')
.replace(/%/g, '')
.split(separator)
.filter((part) => Boolean(part.length));
let f = 255;
let s = 255;
let t = 255;
let a = 255;
if (parts[0]) {
f = parseFloat(parts[0].trim());
}
if (parts[1]) {
s = parseFloat(parts[1].trim());
}
if (parts[2]) {
t = parseFloat(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 } = hslToRgb(h, s, l);
return (a & 0xff) * 0x01000000 + (r & 0xff) * 0x00010000 + (g & 0xff) * 0x00000100 + (b & 0xff);
}
function argbFromHsvOrHsva(value: string): number {
const { f: h, s: s, t: v, a } = parseColorWithAlpha(value);
const { r, g, b } = hsvToRgb(h, s, v);
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]
// *Returns:* { h, s, l } in [0,360] and [0,100]
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
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 * 360, s: s * 100, l: l * 100 };
}
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, 360] and s and l are contained [0, 100]
// *Returns:* { r, g, b } in the set [0, 255]
function hslToRgb(h1, s1, l1) {
const h = (h1 % 360) / 360;
const s = s1 / 100;
const l = l1 / 100;
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: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
}
// `rgbToHsv`
// Converts an RGB color value to HSV
// *Assumes:* r, g, and b are contained in the set [0, 255]
// *Returns:* { h, s, v } in [0,360] and [0,100]
function rgbToHsv(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
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 * 360, s: s * 100, v: v * 100 };
}
// `hsvToRgb`
// Converts an HSV color value to RGB.
// *Assumes:* h is contained in [0, 360] and s and v are contained [0, 100]
// *Returns:* { r, g, b } in the set [0, 255]
function hsvToRgb(h1, s1, v1) {
const h = ((h1 % 360) / 360) * 6;
const s = s1 / 100;
const v = v1 / 100;
const i = Math.floor(h),
f = h - i,
p = v * (1 - s),
q = v * (1 - f * s),
t = v * (1 - (1 - f) * s),
mod = i % 6,
r = [v, q, p, p, t, v][mod],
g = [t, v, v, q, p, p][mod],
b = [p, p, t, v, v, q][mod];
return { r: r * 255, g: g * 255, b: b * 255 };
}

View File

@ -0,0 +1,253 @@
import { color } from '@csstools/css-color-parser';
import { parseComponentValue } from '@csstools/css-parser-algorithms';
import { serializeRGB } from '@csstools/css-color-parser';
import { tokenize } from '@csstools/css-tokenizer';
export const HEX_REGEX = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)|(^#[0-9A-F]{8}$)|(^#[0-9A-F]{4}$)/i;
export function isCssColorMixExpression(value: string) {
return value.includes('color-mix(');
}
export function argbFromColorMix(value: string): number {
const astComponentValue = parseComponentValue(tokenize({ css: value }));
const colorData = color(astComponentValue);
let argb: number;
if (colorData) {
const serialized = serializeRGB(colorData);
argb = argbFromRgbOrRgba(serialized.toString());
} else {
argb = -1;
}
return argb;
}
export function fromArgbToRgba(argb: number): { a: number; r: number; g: number; b: number } {
return {
a: (argb >> 24) & 0xff,
r: (argb >> 16) & 0xff,
g: (argb >> 8) & 0xff,
b: argb & 0xff,
};
}
export function isRgbOrRgba(value: string): boolean {
return (value.startsWith('rgb(') || value.startsWith('rgba(')) && value.endsWith(')');
}
export function isHslOrHsla(value: string): boolean {
return (value.startsWith('hsl') || value.startsWith('hsla(')) && value.endsWith(')');
}
export function isHsvOrHsva(value: string): boolean {
return (value.startsWith('hsv') || value.startsWith('hsva(')) && value.endsWith(')');
}
export function parseColorWithAlpha(value: string): any {
const separator = value.indexOf(',') !== -1 ? ',' : ' ';
const parts = value
.replace(/(rgb|hsl|hsv)a?\(/, '')
.replace(')', '')
.replace(/\//, ' ')
.replace(/%/g, '')
.split(separator)
.filter((part) => Boolean(part.length));
let f = 255;
let s = 255;
let t = 255;
let a = 255;
if (parts[0]) {
f = parseFloat(parts[0].trim());
}
if (parts[1]) {
s = parseFloat(parts[1].trim());
}
if (parts[2]) {
t = parseFloat(parts[2].trim());
}
if (parts[3]) {
a = Math.round(parseFloat(parts[3].trim()) * 255);
}
return { f, s, t, a };
}
export function argbFromString(hex: string) {
// always called as SHARP as first char
hex = hex.substring(1);
const length = hex.length;
// first we normalize
if (length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
} else if (length === 4) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
}
let intVal = parseInt(hex, 16);
if (hex.length === 6) {
// add the alpha component since the provided string is RRGGBB
intVal = (intVal & 0x00ffffff) + 0xff000000;
} else {
// the new format is #RRGGBBAA
// we need to shift the alpha value to 0x01000000 position
const a = (intVal / 0x00000001) & 0xff;
intVal = (intVal >>> 8) + (a & 0xff) * 0x01000000;
}
return intVal;
}
export 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);
}
export function argbFromHslOrHsla(value: string): number {
const { f: h, s: s, t: l, a } = parseColorWithAlpha(value);
const { r, g, b } = hslToRgb(h, s, l);
return (a & 0xff) * 0x01000000 + (r & 0xff) * 0x00010000 + (g & 0xff) * 0x00000100 + (b & 0xff);
}
export function argbFromHsvOrHsva(value: string): number {
const { f: h, s: s, t: v, a } = parseColorWithAlpha(value);
const { r, g, b } = hsvToRgb(h, s, v);
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]
// *Returns:* { h, s, l } in [0,360] and [0,100]
export function rgbToHsl(r: number, g: number, b: number) {
r /= 255;
g /= 255;
b /= 255;
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 * 360, s: s * 100, l: l * 100 };
}
export function hue2rgb(p: number, q: number, t: number) {
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, 360] and s and l are contained [0, 100]
// *Returns:* { r, g, b } in the set [0, 255]
export function hslToRgb(h1: number, s1: number, l1: number) {
const h = (h1 % 360) / 360;
const s = s1 / 100;
const l = l1 / 100;
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: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
}
// `rgbToHsv`
// Converts an RGB color value to HSV
// *Assumes:* r, g, and b are contained in the set [0, 255]
// *Returns:* { h, s, v } in [0,360] and [0,100]
export function rgbToHsv(r: number, g: number, b: number) {
r /= 255;
g /= 255;
b /= 255;
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 * 360, s: s * 100, v: v * 100 };
}
// `hsvToRgb`
// Converts an HSV color value to RGB.
// *Assumes:* h is contained in [0, 360] and s and v are contained [0, 100]
// *Returns:* { r, g, b } in the set [0, 255]
export function hsvToRgb(h1: number, s1: number, v1: number) {
const h = ((h1 % 360) / 360) * 6;
const s = s1 / 100;
const v = v1 / 100;
const i = Math.floor(h),
f = h - i,
p = v * (1 - s),
q = v * (1 - f * s),
t = v * (1 - (1 - f) * s),
mod = i % 6,
r = [v, q, p, p, t, v][mod],
g = [t, v, v, q, p, p][mod],
b = [p, p, t, v, v, q][mod];
return { r: r * 255, g: g * 255, b: b * 255 };
}

View File

@ -53,12 +53,15 @@
"preuninstall": "node cli-hooks/preuninstall.js" "preuninstall": "node cli-hooks/preuninstall.js"
}, },
"dependencies": { "dependencies": {
"@csstools/css-calc": "~2.1.2",
"@csstools/css-color-parser": "^3.0.8",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@nativescript/hook": "~2.0.0", "@nativescript/hook": "~2.0.0",
"acorn": "^8.7.0", "acorn": "^8.7.0",
"css-tree": "^1.1.2", "css-tree": "^1.1.2",
"css-what": "^6.1.0", "css-what": "^6.1.0",
"emoji-regex": "^10.2.1", "emoji-regex": "^10.2.1",
"reduce-css-calc": "^2.1.7",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"nativescript": { "nativescript": {

View File

@ -2,6 +2,9 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"files": [], "files": [],
"include": [], "include": [],
"compilerOptions": {
"moduleResolution": "bundler"
},
"references": [ "references": [
{ {
"path": "./tsconfig.lib.json" "path": "./tsconfig.lib.json"

View File

@ -1,6 +1,4 @@
import { ViewBase } from '../view-base'; import { ViewBase } from '../view-base';
// Types.
import { PropertyChangeData, WrappedValue } from '../../../data/observable'; import { PropertyChangeData, WrappedValue } from '../../../data/observable';
import { Trace } from '../../../trace'; import { Trace } from '../../../trace';
@ -163,20 +161,27 @@ export function _evaluateCssCalcExpression(value: string) {
} }
if (isCssCalcExpression(value)) { if (isCssCalcExpression(value)) {
// Note: reduce-css-calc can't handle certain values return require('@csstools/css-calc').calc(_replaceKeywordsWithValues(_replaceDip(value)));
let cssValue = value.replace(/([0-9]+(\.[0-9]+)?)dip\b/g, '$1');
if (cssValue.includes('unset')) {
cssValue = cssValue.replace(/unset/g, '0');
}
if (cssValue.includes('infinity')) {
cssValue = cssValue.replace(/infinity/g, '999999');
}
return require('reduce-css-calc')(cssValue);
} else { } else {
return value; return value;
} }
} }
function _replaceDip(value: string) {
return value.replace(/([0-9]+(\.[0-9]+)?)dip\b/g, '$1');
}
function _replaceKeywordsWithValues(value: string) {
let cssValue = value;
if (cssValue.includes('unset')) {
cssValue = cssValue.replace(/unset/g, '0');
}
if (cssValue.includes('infinity')) {
cssValue = cssValue.replace(/infinity/g, '999999');
}
return cssValue;
}
function getPropertiesFromMap(map): Property<any, any>[] | CssProperty<any, any>[] { function getPropertiesFromMap(map): Property<any, any>[] | CssProperty<any, any>[] {
const props = []; const props = [];
Object.getOwnPropertySymbols(map).forEach((symbol) => props.push(map[symbol])); Object.getOwnPropertySymbols(map).forEach((symbol) => props.push(map[symbol]));

View File

@ -0,0 +1,36 @@
import { Color } from '../../color';
describe('css-color-mix', () => {
// all examples from:
// https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix
it('color-mix(in oklab, var(--color-black) 50%, transparent)', () => {
const color = new Color('color-mix(in oklab, black 50%, transparent)');
expect(color.toRgbString()).toBe('rgba(0, 0, 0, 0.50)');
});
it('color-mix(in hsl, hsl(200 50 80), coral 80%)', () => {
const color = new Color('color-mix(in hsl, hsl(200 50 80), coral 80%)');
expect(color.toRgbString()).toBe('rgba(247, 103, 149, 1.00)');
});
it('color-mix(in lch longer hue, hsl(200deg 50% 80%), coral)', () => {
const color = new Color('color-mix(in lch longer hue, hsl(200deg 50% 80%), coral)');
expect(color.toRgbString()).toBe('rgba(136, 202, 134, 1.00)');
});
it('color-mix(in srgb, plum, #f00)', () => {
const color = new Color('color-mix(in srgb, plum, #f00)');
expect(color.toRgbString()).toBe('rgba(238, 80, 110, 1.00)');
});
it('color-mix(in lab, plum 60%, #f00 50%)', () => {
const color = new Color('color-mix(in lab, plum 60%, #f00 50%)');
expect(color.toRgbString()).toBe('rgba(247, 112, 125, 1.00)');
});
it('color-mix(in --swop5c, red, blue)', () => {
const color = new Color('color-mix(in --swop5c, red, blue)');
expect(color.toRgbString()).toBe('rgba(0, 0, 255, 0.00)');
});
});