Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/css/types/__tests__/color-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ describe('types', () => {
it('hsl(.75turn, 60%, 70%)', () => strictEqual(parse('hsl(.75turn, 60%, 70%)'), parse('rgb(178,132,224)')));
it('hsla(.75turn, 60%, 70%, 50%)', () =>
strictEqual(parse('hsl(.75turn, 60%, 70%, 50%)'), parse('rgba(178,132,224, 0.5)')));

// CSS Color Module Level 4 color() function tests
it('color(srgb 0 0 0)', () => strictEqual(parse('color(srgb 0 0 0)'), pack(0, 0, 0, 1)));
it('color(srgb 1 1 1)', () => strictEqual(parse('color(srgb 1 1 1)'), pack(255, 255, 255, 1)));
it('color(srgb 0.5 0.5 0.5)', () => strictEqual(parse('color(srgb 0.5 0.5 0.5)'), pack(128, 128, 128, 1)));
it('color(srgb 0 0 0 / 0.5)', () => strictEqual(parse('color(srgb 0 0 0 / 0.5)'), pack(0, 0, 0, 0.5)));
it('color(srgb 0 0 0 / 0.85)', () => strictEqual(parse('color(srgb 0 0 0 / 0.85)'), pack(0, 0, 0, 0.85)));
it('color(srgb 1 0.5 0)', () => strictEqual(parse('color(srgb 1 0.5 0)'), pack(255, 128, 0, 1)));
it('color(display-p3 1 0 0)', () => strictEqual(parse('color(display-p3 1 0 0)'), pack(255, 0, 0, 1)));
it('color(srgb 100% 0% 50%)', () => strictEqual(parse('color(srgb 100% 0% 50%)'), pack(255, 0, 128, 1)));
it('color(srgb 50% 50% 50% / 25%)', () => strictEqual(parse('color(srgb 50% 50% 50% / 25%)'), pack(128, 128, 128, 0.25)));
});
describe('util', () => {
describe('isTransparent', () => {
Expand Down
100 changes: 99 additions & 1 deletion src/css/types/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ const getTokenColorValue = (token: CSSValue, i: number): number => {
return 0;
};

// Helper function for color() function values (0-1 range)
const getColorFunctionValue = (token: CSSValue): number => {
if (token.type === TokenType.NUMBER_TOKEN) {
return token.number;
}

if (token.type === TokenType.PERCENTAGE_TOKEN) {
return token.number / 100;
}

return 0;
};

const rgb = (_context: Context, args: CSSValue[]): number => {
const tokens = args.filter(nonFunctionArgSeparator);

Expand Down Expand Up @@ -143,13 +156,98 @@ const hsl = (context: Context, args: CSSValue[]): number => {
return pack(r * 255, g * 255, b * 255, a);
};

// CSS Color Module Level 4 color() function
const colorFunction = (_context: Context, args: CSSValue[]): number => {
const tokens = args.filter(nonFunctionArgSeparator);

if (tokens.length < 4) {
return 0; // Invalid color function
}

// First token should be the color space identifier
const colorSpace = tokens[0];
if (colorSpace.type !== TokenType.IDENT_TOKEN) {
return 0;
}

const colorSpaceName = colorSpace.value.toLowerCase();

// For now, we'll support srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020
// All will be converted to sRGB for display
const [c1, c2, c3] = tokens.slice(1, 4).map(token => getColorFunctionValue(token));

// Handle alpha channel if present (after slash)
let alpha = 1;
if (tokens.length >= 6) {
// Look for slash separator and alpha value
const slashIndex = tokens.findIndex((token, index) =>
index >= 4 && token.type === TokenType.DELIM_TOKEN && token.value === '/'
);
if (slashIndex !== -1 && slashIndex + 1 < tokens.length) {
const alphaToken = tokens[slashIndex + 1];
alpha = getColorFunctionValue(alphaToken);
}
}

let r: number, g: number, b: number;

switch (colorSpaceName) {
case 'srgb':
// sRGB values are already in 0-1 range
r = Math.round(c1 * 255);
g = Math.round(c2 * 255);
b = Math.round(c3 * 255);
break;
case 'srgb-linear':
// Convert from linear RGB to sRGB
r = Math.round(linearToSrgb(c1) * 255);
g = Math.round(linearToSrgb(c2) * 255);
b = Math.round(linearToSrgb(c3) * 255);
break;
case 'display-p3':
case 'a98-rgb':
case 'prophoto-rgb':
case 'rec2020':
// For simplicity, treat these color spaces as sRGB
// In a full implementation, proper color space conversion would be needed
r = Math.round(c1 * 255);
g = Math.round(c2 * 255);
b = Math.round(c3 * 255);
break;
default:
// Unsupported color space, fallback to treating as sRGB
r = Math.round(c1 * 255);
g = Math.round(c2 * 255);
b = Math.round(c3 * 255);
break;
}

// Clamp values to valid range
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
alpha = Math.max(0, Math.min(1, alpha));

return pack(r, g, b, alpha);
};

// Helper function to convert linear RGB to sRGB
const linearToSrgb = (value: number): number => {
if (value <= 0.0031308) {
return value * 12.92;
} else {
return 1.055 * Math.pow(value, 1 / 2.4) - 0.055;
}
};

const SUPPORTED_COLOR_FUNCTIONS: {
[key: string]: (context: Context, args: CSSValue[]) => number;
} = {
hsl: hsl,
hsla: hsl,
rgb: rgb,
rgba: rgb
rgba: rgb,
color: colorFunction
};

export const parseColor = (context: Context, value: string): Color =>
Expand Down