Skip to content

Commit 12b708a

Browse files
committed
Fixes #68; better handling of hue in differenceEuclidean()
1 parent e0a1428 commit 12b708a

File tree

4 files changed

+82
-14
lines changed

4 files changed

+82
-14
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## Culori Changelog
22

3+
### Unreleased
4+
5+
Stop treating the _alpha_ channel specifically in the Euclidean distance formula. Instead, assume it's always the fourth channel and assign it a default weight of 0. This means that it's now possible to factor in the alpha into the distance, if needed.
6+
7+
There's also a change of handing `NaN` differences on a channel. In this case, the distance on that particular channel is considered to be zero. (Which kind of makes sense?)
8+
9+
Distances on the hue (`h` channel) are now computed with the _shortest hue distance_, taking into account the cyclical nature of this channel. ([#68](https://github.com/Evercoder/culori/issues/68)).
10+
311
### 0.5.4
412

513
Fixed typo in CMC (l:c) color difference formula. ([#66](https://github.com/Evercoder/culori/issues/66))

README.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ The object needs to have a `mode` property that identifies the color space, and
8787
8888
### A note on the API
8989
90-
TODO explain Functional style and its benefits.
90+
> 🕳 explain choice of functional style and its benefits.
9191
9292
### Basic methods
9393
@@ -209,6 +209,8 @@ samples(5).map(grays);
209209
210210
#### Interpolation functions
211211
212+
> 🕳 expand section
213+
212214
<a name="culoriInterpolateFunctionLinear" href="#culoriInterpolateFunctionLinear">#</a> culori.**interpolateFunctionLinear** [<>](https://github.com/evercoder/culori/blob/master/src/interpolate/interpolateFunctionLinear.js 'Source')
213215
214216
<a name="culoriInterpolateFunctionSpline" href="#culoriInterpolateFunctionSpline">#</a> culori.**interpolateFunctionSpline** [<>](https://github.com/evercoder/culori/blob/master/src/interpolate/interpolateFunctionSpline.js 'Source')
@@ -219,6 +221,8 @@ samples(5).map(grays);
219221
220222
#### Interpolation modes
221223
224+
> 🕳 expand section
225+
222226
<a name="culoriInterpolateNumber" href="#culoriInterpolateNumber">#</a> culori.**interpolateNumber** [<>](https://github.com/evercoder/culori/blob/master/src/interpolate/interpolateNumber.js 'Source')
223227
224228
<a name="culoriInterpolateHue" href="#culoriInterpolateHue">#</a> culori.**interpolateHue** [<>](https://github.com/evercoder/culori/blob/master/src/interpolate/interpolateHue.js 'Source')
@@ -229,12 +233,25 @@ samples(5).map(grays);
229233
230234
These methods are concerned to finding the [distance between two colors](https://en.wikipedia.org/wiki/Color_difference) based on various formulas. Each of these formulas will return a _function (colorA, colorB)_ that lets you measure the distance between two colors. Also available as a separate [d3 plugin](https://github.com/evercoder/d3-color-difference).
231235
232-
<a name="culoriDifferenceEuclidean" href="#culoriDifferenceEuclidean">#</a> culori.**differenceEuclidean**(_mode = 'rgb'_, _weights = [1, 1, 1]_) [<>](https://github.com/evercoder/culori/blob/master/src/difference.js 'Source')
236+
<a name="culoriDifferenceEuclidean" href="#culoriDifferenceEuclidean">#</a> culori.**differenceEuclidean**(_mode = 'rgb'_, _weights = [1, 1, 1, 0]_) [<>](https://github.com/evercoder/culori/blob/master/src/difference.js 'Source')
233237
234238
Returns a [Euclidean distance](https://en.wikipedia.org/wiki/Color_difference#Euclidean) function in a certain color space.
235239
236240
You can optionally assign different weights to the channels in the color space. See, for example, the [Kotsarenko/Ramos distance](#culoriDifferenceKotsarenkoRamos).
237241
242+
The default weights `[1, 1, 1, 0]` mean that the _alpha_, which is the fourth channel in all the color spaces Culori defines, is not taken into account. Send `[1, 1, 1, 1]` as the weights to include it in the computation.
243+
244+
For the `h` channel in the color (in any color space that has this channel), we're using a _shortest hue distance_ to compute the hue's contribution to the distance. In spaces such as HSL or HSV, where the range of this difference is `[0, 180]` — as opposed to `[0, 1]` for the other channels — consider adjusting the weights so that the hue contributes "equally" to the distance:
245+
246+
```js
247+
let hsl_distance = culori.differenceEuclidean('hsl', [
248+
1 / (180 * 180),
249+
1,
250+
1,
251+
0
252+
]);
253+
```
254+
238255
<a name="culoriDifferenceCie76" href="#culoriDifferenceCie76">#</a> culori.**differenceCie76**() [<>](https://github.com/evercoder/culori/blob/master/src/difference.js 'Source')
239256
240257
Computes the [CIE76][cie76] ΔE\*<sub>ab</sub> color difference between the colors _a_ and _b_. The computation is done in the Lab color space and it is analogous to [culori.differenceEuclidean('lab')](#culoriDifferenceEuclidean).
@@ -275,6 +292,8 @@ Pass _n = Infinity_ to get all colors in the array with a maximum distance of _
275292
276293
### RGB / LRGB (Linear RGB)
277294
295+
> 🕳 expand this section
296+
278297
### HSL / HSV / HSI
279298
280299
[HSL, HSV, and HSI](https://en.wikipedia.org/wiki/HSL_and_HSV) are a family of representations of the RGB color space, created in 1970 to provide color spaces more closely aligned to how humans perceive colors.
@@ -436,7 +455,14 @@ function contrast(colorA, colorB) {
436455
437456
## Extending culori
438457
439-
TODO
458+
### Defining a color space
459+
460+
**Note:** The order of the items in the `channels` array matters. To keep things simple, we're making the following conventions:
461+
462+
- the fourth item in the array should be `alpha`
463+
- any cyclical values (e.g. hue) should be identified by `h`, in the range `[0, 360)`
464+
465+
These constrains make sure `differenceEuclidean()` works as expected.
440466
441467
## See also
442468

src/difference.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
11
import { getModeDefinition } from './modes';
22
import converter from './converter';
3+
import normalizeHue from './util/normalizeHue';
4+
5+
const hue_difference = (a, b) => {
6+
let na = normalizeHue(a);
7+
let nb = normalizeHue(b);
8+
if (Math.abs(nb - na) > 180) {
9+
// todo should this be normalized once again?
10+
return na - (nb - 360 * Math.sign(nb - na));
11+
}
12+
return na - nb;
13+
};
314

4-
const differenceEuclidean = (mode = 'rgb', weights = [1, 1, 1]) => {
15+
const differenceEuclidean = (mode = 'rgb', weights = [1, 1, 1, 0]) => {
516
let channels = getModeDefinition(mode).channels;
617
let conv = converter(mode);
718
return (std, smp) => {
819
let ConvStd = conv(std);
920
let ConvSmp = conv(smp);
1021
return Math.sqrt(
11-
channels.reduce(
12-
(delta, k, idx) =>
13-
// ignore alpha channel in computing the euclidean distance
14-
delta +
15-
(k === 'alpha'
16-
? 0
17-
: weights[idx] * Math.pow(ConvStd[k] - ConvSmp[k], 2)),
18-
0
19-
)
22+
channels.reduce((sum, k, idx) => {
23+
let delta =
24+
k === 'h'
25+
? hue_difference(ConvStd[k], ConvSmp[k])
26+
: ConvStd[k] - ConvSmp[k];
27+
return (
28+
sum +
29+
(weights[idx] || 0) * Math.pow(isNaN(delta) ? 0 : delta, 2)
30+
);
31+
}, 0)
2032
);
2133
};
2234
};

test/difference.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ let {
99
differenceKotsarenkoRamos,
1010
rgb,
1111
lab,
12-
round
12+
round,
13+
hsl
1314
} = culori;
1415

1516
tape('euclidean distance in RGB', function(test) {
@@ -28,6 +29,27 @@ tape('euclidean distance in RGB', function(test) {
2829
test.end();
2930
});
3031

32+
tape('euclidean distance in HSL', function(test) {
33+
let delta = differenceEuclidean('hsl');
34+
35+
test.equal(
36+
delta(hsl({ h: 0, s: 1, l: 1 }), hsl({ h: 180, s: 1, l: 1 })),
37+
180
38+
);
39+
40+
test.equal(
41+
delta(hsl({ h: 0, s: 1, l: 1 }), hsl({ h: 360, s: 1, l: 1 })),
42+
0
43+
);
44+
45+
test.equal(
46+
delta(hsl({ h: 60, s: 1, l: 1 }), hsl({ h: -540, s: 1, l: 1 })),
47+
120
48+
);
49+
50+
test.end();
51+
});
52+
3153
tape('cie76 difference', function(test) {
3254
test.equal(
3355
differenceCie76()(

0 commit comments

Comments
 (0)