rgb.js
4.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
'use strict';
const ApiError = require('./ApiError');
module.exports.rgbToXY = (rgb, colorGamut) => {
if (!colorGamut) {
throw new ApiError('No color gamut provided, cannot perform conversion of RGB');
}
return _getXYStateFromRGB(rgb[0], rgb[1], rgb[2], colorGamut);
};
//TODO could re-expose the conversion back to RGB from the XY co-ordinates, but that should not be necessary anymore and it was
// a gross approximation, code is still present below.
class XY {
constructor(x, y) {
this._x = x;
this._y = y;
}
get x() {
return this._x;
}
get y() {
return this._y;
}
crossProduct(xy) {
return (this.x * xy.y) - (this.y * xy.x);
}
}
function _isInColorGamut(p, gamut) {
const v1 = new XY((gamut.green.x - gamut.red.x), (gamut.green.y - gamut.red.y))
, v2 = new XY((gamut.blue.x - gamut.red.x) , (gamut.blue.y - gamut.red.y))
, q = new XY(p.x - gamut.red.x, p.y - gamut.red.y)
;
const s = q.crossProduct(v2) / v1.crossProduct(v2)
, t = v1.crossProduct(q) / v1.crossProduct(v2)
;
return (s >= 0.0) && (t >= 0.0) && (s + t <= 1.0);
}
/**
* Find the closest point on a line. This point will be reproducible by the limits.
*
* @param start {XY} The point where the line starts.
* @param stop {XY} The point where the line ends.
* @param point {XY} The point which is close to the line.
* @return {XY} A point that is on the line specified, and closest to the XY provided.
*/
function _getClosestPoint(start, stop, point) {
const AP = new XY(point.x - start.x, point.y - start.y)
, AB = new XY(stop.x - start.x, stop.y - start.y)
, ab2 = AB.x * AB.x + AB.y * AB.y
, ap_ab = AP.x * AB.x + AP.y * AB.y
;
let t = ap_ab / ab2;
if (t < 0.0) {
t = 0.0;
} else if (t > 1.0) {
t = 1.0;
}
return new XY((start.x + AB.x * t), (start.y + AB.y * t));
}
function _getDistanceBetweenPoints(pOne, pTwo) {
const dx = pOne.x - pTwo.x
, dy = pOne.y - pTwo.y
;
return Math.sqrt(dx * dx + dy * dy);
}
function _getXYStateFromRGB(red, green, blue, gamut) {
const r = _gammaCorrection(red)
, g = _gammaCorrection(green)
, b = _gammaCorrection(blue)
, X = r * 0.4360747 + g * 0.3850649 + b * 0.0930804
, Y = r * 0.2225045 + g * 0.7168786 + b * 0.0406169
, Z = r * 0.0139322 + g * 0.0971045 + b * 0.7141733
;
let cx = X / (X + Y + Z)
, cy = Y / (X + Y + Z)
;
cx = isNaN(cx) ? 0.0 : cx;
cy = isNaN(cy) ? 0.0 : cy;
let xyPoint = new XY(cx, cy);
if (!_isInColorGamut(xyPoint, gamut)) {
xyPoint = _resolveXYPointForLamp(xyPoint, gamut);
}
return [xyPoint.x, xyPoint.y];
}
/**
* This function is a rough approximation of the reversal of RGB to xy transform. It is a gross approximation and does
* get close, but is not exact.
* @param x
* @param y
* @param brightness
* @returns {Array} RGB values
* @private
*
* This function is a modification of the one found at https://github.com/bjohnso5/hue-hacking/blob/master/src/colors.js#L251
*/
function _getRGBFromXYState(x, y, brightness) {
const Y = brightness
, X = (Y / y) * x
, Z = (Y / y) * (1 - x - y)
;
let rgb = [
X * 1.612 - Y * 0.203 - Z * 0.302,
-X * 0.509 + Y * 1.412 + Z * 0.066,
X * 0.026 - Y * 0.072 + Z * 0.962
];
// Apply reverse gamma correction.
rgb = rgb.map(function (x) {
return (x <= 0.0031308) ? (12.92 * x) : ((1.0 + 0.055) * Math.pow(x, (1.0 / 2.4)) - 0.055);
});
// Bring all negative components to zero.
rgb = rgb.map(function (x) {
return Math.max(0, x);
});
// If one component is greater than 1, weight components by that value.
const max = Math.max(rgb[0], rgb[1], rgb[2]);
if (max > 1) {
rgb = rgb.map(function (x) {
return x / max;
});
}
rgb = rgb.map(function (x) {
return Math.floor(x * 255);
});
return rgb;
}
/**
* When a color is outside the limits, find the closest point on each line in the CIE 1931 'triangle'.
* @param point {XY} The point that is outside the limits
* @param gamut The limits of the bulb (red, green and blue XY points).
* @returns {XY}
*/
function _resolveXYPointForLamp(point, gamut) {
const pAB = _getClosestPoint(gamut.red, gamut.green, point)
, pAC = _getClosestPoint(gamut.blue, gamut.red, point)
, pBC = _getClosestPoint(gamut.green, gamut.blue, point)
, dAB = _getDistanceBetweenPoints(point, pAB)
, dAC = _getDistanceBetweenPoints(point, pAC)
, dBC = _getDistanceBetweenPoints(point, pBC)
;
let lowest = dAB
, closestPoint = pAB
;
if (dAC < lowest) {
lowest = dAC;
closestPoint = pAC;
}
if (dBC < lowest) {
closestPoint = pBC;
}
return closestPoint;
}
function _gammaCorrection(value) {
if (value > 0.04045) {
return Math.pow((value + 0.055) / (1.0 + 0.055), 2.4);
} else {
return value / 12.92;
}
}