Skip to content

Commit 1114606

Browse files
GauravB159aeschli
andauthored
lab() and lch() color previews added (#306)
* Added conversion functions for LAB to RGB for the preview * Added the ability to parse negative numbers since LAB requires it * Added basic tests for LAB, need to add more cases yet * Added lch() conversion functions * Added basic tests for lch(), need to add more * Added lch() conversion functions * Added basic tests for lch(), need to add more * Removed merge conflict * Removed warnings and added reverse conversion functions * Added color presentations for lab and lch to make picker work * Added new tests for lab and lch functions * Ran the built-in formatter and replaced const where appropriate * more tests, fix for xyzToRGB * number can start with + or -. Fix <an+b> parsing --------- Co-authored-by: Martin Aeschlimann <[email protected]>
1 parent 9e3bfe4 commit 1114606

File tree

8 files changed

+343
-20
lines changed

8 files changed

+343
-20
lines changed

src/languageFacts/colors.ts

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,34 @@ export const colorFunctions = [
132132
insertText: 'color-mix(in ${1|hsl,hwb,lch,oklch|} ${2|shorter hue,longer hue,increasing hue,decreasing hue|}, ${3:color} ${4:percentage}, ${5:color} ${6:percentage})',
133133
desc: l10n.t('Mix two colors together in a polar color space.')
134134
},
135+
{
136+
label: 'lab',
137+
func: 'lab($lightness $channel_a $channel_b $alpha)',
138+
insertText: 'lab(${1:lightness} ${2:a} ${3:b} ${4:alpha})',
139+
desc: l10n.t('css.builtin.lab', 'Creates a Color from Lightness, Channel a, Channel b and alpha values.')
140+
},
141+
{
142+
label: 'lab relative',
143+
func: 'lab(from $color $lightness $channel_a $channel_b $alpha)',
144+
insertText: 'lab(from ${1:color} ${2:lightness} ${3:channel_a} ${4:channel_b} ${5:alpha})',
145+
desc: l10n.t('css.builtin.lab', 'Creates a Color from Lightness, Channel a, Channel b and alpha values of another Color.')
146+
},
147+
{
148+
label: 'lch',
149+
func: 'lch($lightness $chrome $hue $alpha)',
150+
insertText: 'lch(${1:lightness} ${2:chrome} ${3:hue} ${4:alpha})',
151+
desc: l10n.t('css.builtin.lab', 'Creates a Color from Lightness, Chroma, Hue and alpha values.')
152+
},
153+
{
154+
label: 'lch relative',
155+
func: 'lch(from $color $lightness $chrome $hue $alpha)',
156+
insertText: 'lch(from ${1:color} ${2:lightness} ${3:chrome} ${4:hue} ${5:alpha})',
157+
desc: l10n.t('css.builtin.lab', 'Creates a Color from Lightness, Chroma, Hue and alpha values of another Color.')
158+
}
159+
135160
];
136161

137-
const colorFunctionNameRegExp = /^(rgb|rgba|hsl|hsla|hwb)$/i;
162+
const colorFunctionNameRegExp = /^(rgb|rgba|hsl|hsla|hwb|lab|lch)$/i;
138163

139164
export const colors: { [name: string]: string } = {
140165
aliceblue: '#f0f8ff',
@@ -296,15 +321,15 @@ export const colorKeywords: { [name: string]: string } = {
296321

297322
const colorKeywordsRegExp = new RegExp(`^(${Object.keys(colorKeywords).join('|')})$`, "i");
298323

299-
function getNumericValue(node: nodes.Node, factor: number) {
324+
function getNumericValue(node: nodes.Node, factor: number, lowerLimit: number = 0, upperLimit: number = 1) {
300325
const val = node.getText();
301326
const m = val.match(/^([-+]?[0-9]*\.?[0-9]+)(%?)$/);
302327
if (m) {
303328
if (m[2]) {
304329
factor = 100.0;
305330
}
306331
const result = parseFloat(m[1]) / factor;
307-
if (result >= 0 && result <= 1) {
332+
if (result >= lowerLimit && result <= upperLimit) {
308333
return result;
309334
}
310335
}
@@ -533,6 +558,186 @@ export function hwbFromColor(rgba: Color): HWBA {
533558
};
534559
}
535560

561+
export interface XYZ { x: number; y: number; z: number; alpha: number; }
562+
563+
export interface RGB { r: number; g: number; b: number; alpha: number; }
564+
565+
export function xyzFromLAB(lab: LAB): XYZ {
566+
const xyz: XYZ = {
567+
x: 0,
568+
y: 0,
569+
z: 0,
570+
alpha: lab.alpha ?? 1
571+
};
572+
xyz.y = (lab.l + 16.0) / 116.0;
573+
xyz.x = (lab.a / 500.0) + xyz.y;
574+
xyz.z = xyz.y - (lab.b / 200.0);
575+
let key: keyof XYZ;
576+
577+
for (key in xyz) {
578+
let pow = xyz[key] * xyz[key] * xyz[key];
579+
if (pow > 0.008856) {
580+
xyz[key] = pow;
581+
} else {
582+
xyz[key] = (xyz[key] - 16.0 / 116.0) / 7.787;
583+
}
584+
}
585+
586+
xyz.x = xyz.x * 95.047;
587+
xyz.y = xyz.y * 100.0;
588+
xyz.z = xyz.z * 108.883;
589+
return xyz;
590+
}
591+
592+
export function xyzToRGB(xyz: XYZ): Color {
593+
const x = xyz.x / 100;
594+
const y = xyz.y / 100;
595+
const z = xyz.z / 100;
596+
597+
const r = 3.2406254773200533 * x - 1.5372079722103187 * y - 0.4986285986982479 * z;
598+
const g = -0.9689307147293197 * x + 1.8757560608852415 * y + 0.041517523842953964 * z;
599+
const b = 0.055710120445510616 * x + -0.2040210505984867 * y + 1.0569959422543882 * z;
600+
601+
const compand = (c: number) => {
602+
return c <= 0.0031308 ?
603+
12.92 * c :
604+
Math.min(1.055 * Math.pow(c, 1 / 2.4) - 0.055, 1);
605+
}
606+
607+
return {
608+
red: Math.round(compand(r) * 255.0),
609+
blue: Math.round(compand(b) * 255.0),
610+
green: Math.round(compand(g) * 255.0),
611+
alpha: xyz.alpha
612+
};
613+
}
614+
615+
export function RGBtoXYZ(rgba: Color): XYZ {
616+
let r: number = rgba.red,
617+
g: number = rgba.green,
618+
b: number = rgba.blue;
619+
620+
if (r > 0.04045) {
621+
r = Math.pow((r + 0.055) / 1.055, 2.4);
622+
} else {
623+
r = r / 12.92;
624+
}
625+
if (g > 0.04045) {
626+
g = Math.pow((g + 0.055) / 1.055, 2.4);
627+
} else {
628+
g = g / 12.92;
629+
}
630+
if (b > 0.04045) {
631+
b = Math.pow((b + 0.055) / 1.055, 2.4);
632+
} else {
633+
b = b / 12.92;
634+
}
635+
r = r * 100;
636+
g = g * 100;
637+
b = b * 100;
638+
639+
//Observer = 2°, Illuminant = D65
640+
const x = r * 0.4124 + g * 0.3576 + b * 0.1805;
641+
const y = r * 0.2126 + g * 0.7152 + b * 0.0722;
642+
const z = r * 0.0193 + g * 0.1192 + b * 0.9505;
643+
return { x, y, z, alpha: rgba.alpha };
644+
}
645+
646+
export function XYZtoLAB(xyz: XYZ, round: Boolean = true): LAB {
647+
const ref_X = 95.047, ref_Y = 100.000, ref_Z = 108.883;
648+
649+
let x: number = xyz.x / ref_X,
650+
y: number = xyz.y / ref_Y,
651+
z: number = xyz.z / ref_Z;
652+
653+
if (x > 0.008856) {
654+
x = Math.pow(x, 1 / 3);
655+
} else {
656+
x = (7.787 * x) + (16 / 116);
657+
}
658+
if (y > 0.008856) {
659+
y = Math.pow(y, 1 / 3);
660+
} else {
661+
y = (7.787 * y) + (16 / 116);
662+
}
663+
if (z > 0.008856) {
664+
z = Math.pow(z, 1 / 3);
665+
} else {
666+
z = (7.787 * z) + (16 / 116);
667+
}
668+
const l: number = (116 * y) - 16,
669+
a: number = 500 * (x - y),
670+
b: number = 200 * (y - z);
671+
if (round) {
672+
return {
673+
l: Math.round((l + Number.EPSILON) * 100) / 100,
674+
a: Math.round((a + Number.EPSILON) * 100) / 100,
675+
b: Math.round((b + Number.EPSILON) * 100) / 100,
676+
alpha: xyz.alpha
677+
};
678+
} else {
679+
return {
680+
l, a, b,
681+
alpha: xyz.alpha
682+
};
683+
}
684+
}
685+
686+
export function labFromColor(rgba: Color, round: Boolean = true): LAB {
687+
const xyz: XYZ = RGBtoXYZ(rgba);
688+
const lab: LAB = XYZtoLAB(xyz, round);
689+
return lab;
690+
}
691+
export function lchFromColor(rgba: Color): LCH {
692+
const lab: LAB = labFromColor(rgba, false);
693+
const c: number = Math.sqrt(Math.pow(lab.a, 2) + Math.pow(lab.b, 2));
694+
let h: number = Math.atan2(lab.b, lab.a) * (180 / Math.PI);
695+
while (h < 0) {
696+
h = h + 360;
697+
}
698+
return {
699+
l: Math.round((lab.l + Number.EPSILON) * 100) / 100,
700+
c: Math.round((c + Number.EPSILON) * 100) / 100,
701+
h: Math.round((h + Number.EPSILON) * 100) / 100,
702+
alpha: lab.alpha
703+
};
704+
}
705+
706+
export function colorFromLAB(l: number, a: number, b: number, alpha: number = 1.0): Color {
707+
const lab: LAB = {
708+
l,
709+
a,
710+
b,
711+
alpha
712+
};
713+
const xyz = xyzFromLAB(lab);
714+
const rgb = xyzToRGB(xyz);
715+
return {
716+
red: (rgb.red >= 0 ? (rgb.red <= 255 ? rgb.red : 255) : 0) / 255.0,
717+
green: (rgb.green >= 0 ? (rgb.green <= 255 ? rgb.green : 255) : 0) / 255.0,
718+
blue: (rgb.blue >= 0 ? (rgb.blue <= 255 ? rgb.blue : 255) : 0) / 255.0,
719+
alpha
720+
};
721+
}
722+
723+
export interface LAB { l: number; a: number; b: number; alpha?: number; }
724+
725+
export function labFromLCH(l: number, c: number, h: number, alpha: number = 1.0): LAB {
726+
return {
727+
l: l,
728+
a: c * Math.cos(h * (Math.PI / 180)),
729+
b: c * Math.sin(h * (Math.PI / 180)),
730+
alpha: alpha
731+
};
732+
}
733+
734+
export function colorFromLCH(l: number, c: number, h: number, alpha: number = 1.0): Color {
735+
const lab: LAB = labFromLCH(l, c, h, alpha);
736+
return colorFromLAB(lab.l, lab.a, lab.b, alpha);
737+
}
738+
739+
export interface LCH { l: number; c: number; h: number; alpha?: number; }
740+
536741
export function getColorValue(node: nodes.Node): Color | null {
537742
if (node.type === nodes.NodeType.HexColorValue) {
538743
const text = node.getText();
@@ -578,6 +783,18 @@ export function getColorValue(node: nodes.Node): Color | null {
578783
const w = getNumericValue(colorValues[1], 100.0);
579784
const b = getNumericValue(colorValues[2], 100.0);
580785
return colorFromHWB(h, w, b, alpha);
786+
} else if (name === 'lab') {
787+
// Reference: https://mina86.com/2021/srgb-lab-lchab-conversions/
788+
const l = getNumericValue(colorValues[0], 100.0);
789+
// Since these two values can be negative, a lower limit of -1 has been added
790+
const a = getNumericValue(colorValues[1], 125.0, -1);
791+
const b = getNumericValue(colorValues[2], 125.0, -1);
792+
return colorFromLAB(l * 100, a * 125, b * 125, alpha);
793+
} else if (name === 'lch') {
794+
const l = getNumericValue(colorValues[0], 100.0);
795+
const c = getNumericValue(colorValues[1], 230.0);
796+
const h = getAngle(colorValues[2]);
797+
return colorFromLCH(l * 100, c * 230, h, alpha);
581798
}
582799
} catch (e) {
583800
// parse error on numeric value

src/parser/cssNodes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export enum NodeType {
101101
PropertyAtRule,
102102
Container,
103103
ModuleConfig,
104+
SelectorList
104105
}
105106

106107
export enum ReferenceType {

src/parser/cssParser.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,7 +1688,7 @@ export class Parser {
16881688
if (node) {
16891689
if (!this.hasWhitespace() && this.accept(TokenType.ParenthesisL)) {
16901690
const tryAsSelector = () => {
1691-
const selectors = this.create(nodes.Node);
1691+
const selectors = this.createNode(nodes.NodeType.SelectorList);
16921692
if (!selectors.addChild(this._parseSelector(true))) {
16931693
return null;
16941694
}
@@ -1704,9 +1704,11 @@ export class Parser {
17041704

17051705
let hasSelector = node.addChild(this.try(tryAsSelector));
17061706
if (!hasSelector) {
1707-
if (
1708-
node.addChild(this._parseBinaryExpr()) &&
1709-
this.acceptIdent('of') &&
1707+
// accept the <an+b> syntax (not a proper expression) https://drafts.csswg.org/css-syntax/#anb
1708+
while (!this.peekIdent('of') && (node.addChild(this._parseTerm()) || node.addChild(this._parseOperator()))) {
1709+
// loop
1710+
}
1711+
if (this.acceptIdent('of') &&
17101712
!node.addChild(this.try(tryAsSelector))
17111713
) {
17121714
return this.finish(node, ParseError.SelectorExpected);

src/parser/cssScanner.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,15 +447,21 @@ export class Scanner {
447447
}
448448

449449
private _number(): boolean {
450-
let npeek = 0, ch: number;
451-
if (this.stream.peekChar() === _DOT) {
452-
npeek = 1;
450+
let npeek = 0;
451+
let hasDot = false;
452+
const peekFirst = this.stream.peekChar();
453+
if (peekFirst === _PLS || peekFirst === _MIN) {
454+
npeek++;
453455
}
454-
ch = this.stream.peekChar(npeek);
456+
if (this.stream.peekChar(npeek) === _DOT) {
457+
npeek++;
458+
hasDot = true;
459+
}
460+
const ch = this.stream.peekChar(npeek);
455461
if (ch >= _0 && ch <= _9) {
456462
this.stream.advance(npeek + 1);
457463
this.stream.advanceWhileChar((ch) => {
458-
return ch >= _0 && ch <= _9 || npeek === 0 && ch === _DOT;
464+
return ch >= _0 && ch <= _9 || !hasDot && ch === _DOT;
459465
});
460466
return true;
461467
}

src/services/cssNavigation.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import * as l10n from '@vscode/l10n';
1212
import * as nodes from '../parser/cssNodes';
1313
import { Symbols } from '../parser/cssSymbolScope';
14-
import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/facts';
14+
import { getColorValue, hslFromColor, hwbFromColor, labFromColor, lchFromColor } from '../languageFacts/facts';
1515
import { startsWith } from '../utils/strings';
1616
import { dirname, joinPath } from '../utils/resources';
1717

@@ -343,6 +343,21 @@ export class CSSNavigation {
343343
}
344344
result.push({ label: label, textEdit: TextEdit.replace(range, label) });
345345

346+
const lab = labFromColor(color);
347+
if (lab.alpha === 1) {
348+
label = `lab(${lab.l}% ${lab.a} ${lab.b})`;
349+
} else {
350+
label = `lab(${lab.l}% ${lab.a} ${lab.b} / ${lab.alpha})`;
351+
}
352+
result.push({ label: label, textEdit: TextEdit.replace(range, label) });
353+
354+
const lch = lchFromColor(color);
355+
if (lab.alpha === 1) {
356+
label = `lch(${lch.l}% ${lch.c} ${lch.h})`;
357+
} else {
358+
label = `lch(${lch.l}% ${lch.c} ${lch.h} / ${lch.alpha})`;
359+
}
360+
result.push({ label: label, textEdit: TextEdit.replace(range, label) });
346361
return result;
347362
}
348363

src/services/selectorPrinting.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -468,9 +468,10 @@ export class SelectorPrinting {
468468
// https://www.w3.org/TR/selectors-4/#the-nth-child-pseudo
469469
specificity.attr++;
470470

471-
// 23 = Binary Expression.
472-
if (childElements.length === 3 && childElements[1].type === 23) {
473-
let mostSpecificListItem = calculateMostSpecificListItem(childElements[2].getChildren());
471+
const lastChild = childElements[childElements.length - 1];
472+
if (childElements.length > 2 && lastChild.type === nodes.NodeType.SelectorList) {
473+
// e.g :nth-child(-n+3 of li.important)
474+
let mostSpecificListItem = calculateMostSpecificListItem(lastChild.getChildren());
474475

475476
specificity.id += mostSpecificListItem.id;
476477
specificity.attr += mostSpecificListItem.attr;

0 commit comments

Comments
 (0)