Skip to content

Commit

Permalink
Support hue interpolation method (#30)
Browse files Browse the repository at this point in the history
Fix #27
  • Loading branch information
asamuzaK authored Dec 22, 2024
1 parent 9304f66 commit 5c64ed3
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 9 deletions.
33 changes: 27 additions & 6 deletions src/js/color.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
*/

import { getType, isString } from './common.js';
import { interpolateHue } from './util.js';

/* constants */
import {
ANGLE, CS_MIX, CS_RGB, CS_XYZ, FUNC_COLOR, FUNC_MIX, NONE, NUM, PCT,
SYN_COLOR_TYPE, SYN_FUNC_COLOR, SYN_HSL, SYN_HSL_LV3, SYN_LAB, SYN_LCH,
ANGLE, CS_HUE_CAPT, CS_MIX, CS_RGB, CS_XYZ, FUNC_COLOR, FUNC_MIX, NONE, NUM,
PCT, SYN_COLOR_TYPE, SYN_FUNC_COLOR, SYN_HSL, SYN_HSL_LV3, SYN_LAB, SYN_LCH,
SYN_MIX, SYN_MIX_CAPT, SYN_RGB, SYN_RGB_LV3, VAL_COMP, VAL_SPEC
} from './constant.js';
const VAL_MIX = 'mixValue';
Expand Down Expand Up @@ -107,6 +108,7 @@ const MATRIX_PROPHOTO_TO_XYZ_D50 = [

/* regexp */
const REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`);
const REG_CS_HUE = new RegExp(`^${CS_HUE_CAPT}$`);
const REG_CURRENT = /^currentColor$/i;
const REG_FUNC_COLOR = new RegExp(`^color\\(\\s*(${SYN_FUNC_COLOR})\\s*\\)$`);
const REG_HSL = new RegExp(`^hsla?\\(\\s*(${SYN_HSL}|${SYN_HSL_LV3})\\s*\\)$`);
Expand Down Expand Up @@ -2683,10 +2685,15 @@ export const resolveColorMix = (value, opt = {}) => {
return ['rgb', 0, 0, 0, 0];
}
}
let colorSpace, colorA, pctA, colorB, pctB;
let colorSpace, hueArc, colorA, pctA, colorB, pctB;
if (nestedItems.length && format === VAL_SPEC) {
const regColorSpace = new RegExp(`^color-mix\\(\\s*in\\s+(${CS_MIX})\\s*,`);
[, colorSpace] = value.match(regColorSpace);
const [, cs] = value.match(regColorSpace);
if (REG_CS_HUE.test(cs)) {
[, colorSpace, hueArc] = REG_CS_HUE.exec(cs);
} else {
colorSpace = cs;
}
if (nestedItems.length === 2) {
const itemA = nestedItems[0].replace(/(?=[()])/g, '\\');
const regA = new RegExp(`(${itemA})(?:\\s+(${PCT}))?`);
Expand Down Expand Up @@ -2724,7 +2731,11 @@ export const resolveColorMix = (value, opt = {}) => {
const reg = new RegExp(`^(${SYN_COLOR_TYPE})(?:\\s+(${PCT}))?$`);
[, colorA, pctA] = colorPartA.match(reg);
[, colorB, pctB] = colorPartB.match(reg);
colorSpace = cs;
if (REG_CS_HUE.test(cs)) {
[, colorSpace, hueArc] = REG_CS_HUE.exec(cs);
} else {
colorSpace = cs;
}
}
// normalize percentages and set multipler
let pA, pB, m;
Expand Down Expand Up @@ -2856,7 +2867,11 @@ export const resolveColorMix = (value, opt = {}) => {
valueA += ` ${pA}%`;
}
}
return `color-mix(in ${colorSpace}, ${valueA}, ${valueB})`;
if (hueArc) {
return `color-mix(in ${colorSpace} ${hueArc} hue, ${valueA}, ${valueB})`;
} else {
return `color-mix(in ${colorSpace}, ${valueA}, ${valueB})`;
}
}
let r, g, b, alpha;
// in srgb, srgb-linear
Expand Down Expand Up @@ -3042,6 +3057,9 @@ export const resolveColorMix = (value, opt = {}) => {
[[hA, sA, lA, alphaA], [hB, sB, lB, alphaB]] =
normalizeColorComponents([hA, sA, lA, alphaA], [hB, sB, lB, alphaB],
true);
if (hueArc) {
[hA, hB] = interpolateHue(hA, hB, hueArc);
}
const factorA = alphaA * pA;
const factorB = alphaB * pB;
alpha = (factorA + factorB);
Expand Down Expand Up @@ -3189,6 +3207,9 @@ export const resolveColorMix = (value, opt = {}) => {
[[lA, cA, hA, alphaA], [lB, cB, hB, alphaB]] =
normalizeColorComponents([lA, cA, hA, alphaA], [lB, cB, hB, alphaB],
true);
if (hueArc) {
[hA, hB] = interpolateHue(hA, hB, hueArc);
}
const factorA = alphaA * pA;
const factorB = alphaB * pB;
alpha = (factorA + factorB);
Expand Down
12 changes: 9 additions & 3 deletions src/js/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
/* constants */
const _DIGIT = '(?:0|[1-9]\\d*)';
export const ANGLE = 'deg|g?rad|turn';
export const CS_HUE_ARC = '(?:de|in)creasing|longer|shorter';
export const CS_HUE_NAME = '(?:ok)?lch|hsl|hwb';
export const CS_HUE = `(?:${CS_HUE_NAME})(?:\\s(?:${CS_HUE_ARC})\\shue)?`;
export const CS_HUE_CAPT = `(${CS_HUE_NAME})(?:\\s(${CS_HUE_ARC})\\shue)?`;
export const CS_LAB = '(?:ok)?lab';
export const CS_LCH = '(?:ok)?lch';
export const CS_SRGB = 'srgb(?:-linear)?';
export const CS_RGB = `(?:a98|prophoto)-rgb|display-p3|rec2020|${CS_SRGB}`;
export const CS_XYZ = 'xyz(?:-d(?:50|65))?';
export const CS_MIX = `(?:ok)?l(?:ab|ch)|h(?:sl|wb)|${CS_SRGB}|${CS_XYZ}`;
export const FUNC_CALC_ESC =
'^(?:abs|calc|sign)\\(|(?<=[*\\/\\s\\(])(?:abs|calc|sign)\\(';
export const CS_MIX = `${CS_HUE}|${CS_LAB}|${CS_SRGB}|${CS_XYZ}`;
export const FUNC_CALC = '(?:abs|calc|sign)\\(';
export const FUNC_CALC_ESC = `^${FUNC_CALC}|(?<=[*\\/\\s\\(])${FUNC_CALC}`;
export const FUNC_CALC_VAR_ESC = '(?:abs|calc|sign|var)\\(';
export const FUNC_COLOR = 'color(';
export const FUNC_MIX = 'color-mix(';
Expand Down
49 changes: 49 additions & 0 deletions src/js/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { isString } from './common.js';
/* constants */
import { NAMED_COLORS } from './color.js';
import { SYN_COLOR_TYPE, SYN_MIX } from './constant.js';
const DEG = 360;
const DEG_HALF = 180;

/* regexp */
const REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`);
Expand Down Expand Up @@ -65,3 +67,50 @@ export const valueToJsonString = (value, func = false) => {
});
return res;
};

/**
* interpolate hue
* @param {number} hueA - hue
* @param {number} hueB - hue
* @param {string} arc - arc
* @returns {Array} - [hueA, hueB]
*/
export const interpolateHue = (hueA, hueB, arc = 'shorter') => {
if (!Number.isFinite(hueA)) {
throw new TypeError(`${hueA} is not a number.`)
}
if (!Number.isFinite(hueB)) {
throw new TypeError(`${hueB} is not a number.`)
}
switch (arc) {
case 'decreasing': {
if (hueB > hueA) {
hueA += DEG;
}
break;
}
case 'increasing': {
if (hueB < hueA) {
hueB += DEG;
}
break;
}
case 'longer': {
if (hueB > hueA && hueB < hueA + DEG_HALF) {
hueA += DEG;
} else if (hueB > hueA + DEG_HALF * -1 && hueB <= hueA) {
hueB += DEG;
}
break;
}
case 'shorter':
default: {
if (hueB > hueA + DEG_HALF) {
hueA += DEG;
} else if (hueB < hueA + DEG_HALF * -1) {
hueB += DEG;
}
}
}
return [hueA, hueB];
};
178 changes: 178 additions & 0 deletions test/color.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6269,6 +6269,11 @@ describe('resolve color-mix()', () => {
assert.deepEqual(res, ['rgb', 133, 102, 71, 0.8], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hsl shorter hue, hsl(120 10% 20% / .4) 0%, hsl(30 30% 40% / .8))');
assert.deepEqual(res, ['rgb', 133, 102, 71, 0.8], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hwb, foo, red)');
assert.deepEqual(res, ['rgb', 0, 0, 0, 0], 'result');
Expand Down Expand Up @@ -6404,6 +6409,13 @@ describe('resolve color-mix()', () => {
assert.deepEqual(res, ['srgb', 0.752941, 0.752941, 0, 0], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hwb shorter hue, hwb(120 0% 49.8039% / 0), color(srgb 1 0 0 / 0))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.752941, 0.752941, 0, 0], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lab, foo, red)');
assert.deepEqual(res, ['rgb', 0, 0, 0, 0], 'result');
Expand Down Expand Up @@ -6534,6 +6546,13 @@ describe('resolve color-mix()', () => {
assert.deepEqual(res, ['lch', 50.2841, 87.4109, 87.6208, 0], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lch shorter hue, rgb(255 0 0 / 0), rgb(0 128 0 / 0))', {
format: 'computedValue'
});
assert.deepEqual(res, ['lch', 50.2841, 87.4109, 87.6208, 0], 'result');
});

it('should get value', () => {
const res = func('color-mix(in oklab, foo, red)');
assert.deepEqual(res, ['rgb', 0, 0, 0, 0], 'result');
Expand Down Expand Up @@ -6680,6 +6699,14 @@ describe('resolve color-mix()', () => {
'result');
});

it('should get value', () => {
const res = func('color-mix(in oklch shorter hue, rgb(255 0 0 / 0), rgb(0 128 0 / 0))', {
format: 'computedValue'
});
assert.deepEqual(res, ['oklch', 0.573854, 0.217271, 85.8646, 0],
'result');
});

it('should get value', () => {
const res = func('color-mix(in srgb, color-mix(in srgb, red, blue), color-mix(in srgb, transparent, #008000))', {
format: 'computedValue'
Expand Down Expand Up @@ -6765,6 +6792,38 @@ describe('resolve color-mix()', () => {
'result');
});

it('should get value', () => {
const res = func('color-mix(in lch, color-mix(in lch, lab(46.2775% -47.5621 48.5837 / .5), blue), purple)', {
format: 'computedValue'
});
assert.deepEqual(res, ['lch', 32.0257, 85.385, 272.492, 0.875], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lch shorter hue, color-mix(in lch, lab(46.2775% -47.5621 48.5837 / .5), blue), purple)', {
format: 'computedValue'
});
assert.deepEqual(res, ['lch', 32.0257, 85.385, 272.492, 0.875], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lch, color-mix(in lch, lab(46.2775% -47.5621 48.5837 / .5), blue), purple)', {
format: 'specifiedValue'
});
assert.deepEqual(res,
'color-mix(in lch, color-mix(in lch, lab(46.2775 -47.5621 48.5837 / 0.5), blue), purple)',
'result');
});

it('should get value', () => {
const res = func('color-mix(in lch shorter hue, color-mix(in lch shorter hue, lab(46.2775% -47.5621 48.5837 / .5), blue), purple)', {
format: 'specifiedValue'
});
assert.deepEqual(res,
'color-mix(in lch shorter hue, color-mix(in lch shorter hue, lab(46.2775 -47.5621 48.5837 / 0.5), blue), purple)',
'result');
});

it('should get value', () => {
const res = func('color-mix(in srgb, transparent, foo)', {
format: 'computedValue'
Expand Down Expand Up @@ -7294,4 +7353,123 @@ describe('resolve color-mix()', () => {
});
assert.deepEqual(res, ['lab', 30, 40, 50, 'none'], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hsl shorter hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.74902, 0.666667, 0.25098, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hsl longer hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.25098, 0.333333, 0.74902, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hsl increasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.74902, 0.666667, 0.25098, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hsl decreasing hue, hsl(40deg 50% 50%), hsl(60deg 50% 50%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.25098, 0.333333, 0.74902, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hwb shorter hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.6, 0.54902, 0.301961, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hwb shorter hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.6, 0.54902, 0.301961, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hwb longer hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.301961, 0.34902, 0.6, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hwb increasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.6, 0.54902, 0.301961, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in hwb decreasing hue, hwb(40deg 30% 40%), hwb(60deg 30% 40%))', {
format: 'computedValue'
});
assert.deepEqual(res, ['srgb', 0.301961, 0.34902, 0.6, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lch shorter hue, lch(100 0 40deg), lch(100 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['lch', 100, 0, 50, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lch longer hue, lch(100 0 40deg), lch(100 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['lch', 100, 0, 230, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lch increasing hue, lch(100 0 40deg), lch(100 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['lch', 100, 0, 50, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in lch decreasing hue, lch(100 0 40deg), lch(100 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['lch', 100, 0, 230, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in oklch shorter hue, oklch(1 0 40deg), oklch(1 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['oklch', 1, 0, 50, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in oklch longer hue, oklch(1 0 40deg), oklch(1 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['oklch', 1, 0, 230, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in oklch increasing hue, oklch(1 0 40deg), oklch(1 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['oklch', 1, 0, 50, 1], 'result');
});

it('should get value', () => {
const res = func('color-mix(in oklch decreasing hue, oklch(1 0 40deg), oklch(1 0 60deg))', {
format: 'computedValue'
});
assert.deepEqual(res, ['oklch', 1, 0, 230, 1], 'result');
});
});
Loading

0 comments on commit 5c64ed3

Please sign in to comment.