"use strict"; var XY = function (x, y) { this.x = x; this.y = y; } , hueLimits = { red: new XY(0.675, 0.322), green: new XY(0.4091, 0.518), blue: new XY(0.167, 0.04) } , livingColorsLimits = { red: new XY(0.704, 0.296), green: new XY(0.2151, 0.7106), blue: new XY(0.138, 0.08) } , defaultLimits = { red: new XY(1.0, 0), green: new XY(0.0, 1.0), blue: new XY(0.0, 0.0) } ; function _crossProduct(p1, p2) { return (p1.x * p2.y - p1.y * p2.x); } function _isInColorGamut(p, lampLimits) { var v1 = new XY( lampLimits.green.x - lampLimits.red.x , lampLimits.green.y - lampLimits.red.y ) , v2 = new XY( lampLimits.blue.x - lampLimits.red.x , lampLimits.blue.y - lampLimits.red.y ) , q = new XY(p.x - lampLimits.red.x, p.y - lampLimits.red.y) , s = _crossProduct(q, v2) / _crossProduct(v1, v2) , t = _crossProduct(v1, q) / _crossProduct(v1, 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) { var 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 , 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) { var dx = pOne.x - pTwo.x , dy = pOne.y - pTwo.y ; return Math.sqrt(dx * dx + dy * dy); } function _getXYStateFromRGB(red, green, blue, limits) { var 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 , cx = X / (X + Y + Z) , cy = Y / (X + Y + Z) , xyPoint ; cx = isNaN(cx) ? 0.0 : cx; cy = isNaN(cy) ? 0.0 : cy; xyPoint = new XY(cx, cy); if (!_isInColorGamut(xyPoint, limits)) { xyPoint = _resolveXYPointForLamp(xyPoint, limits); } 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) { var Y = brightness , X = (Y / y) * x , Z = (Y / y) * (1 - x - y) , 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. var 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 limits The limits of the bulb (red, green and blue XY points). * @returns {XY} */ function _resolveXYPointForLamp(point, limits) { var pAB = _getClosestPoint(limits.red, limits.green, point) , pAC = _getClosestPoint(limits.blue, limits.red, point) , pBC = _getClosestPoint(limits.green, limits.blue, point) , dAB = _getDistanceBetweenPoints(point, pAB) , dAC = _getDistanceBetweenPoints(point, pAC) , dBC = _getDistanceBetweenPoints(point, pBC) , lowest = dAB , closestPoint = pAB ; if (dAC < lowest) { lowest = dAC; closestPoint = pAC; } if (dBC < lowest) { closestPoint = pBC; } return closestPoint; } function _gammaCorrection(value) { var result = value; if (value > 0.04045) { result = Math.pow((value + 0.055) / (1.0 + 0.055), 2.4); } else { result = value / 12.92; } return result; } function _getLimits(modelid) { var limits = defaultLimits ; if (modelid) { modelid = modelid.toLowerCase(); if (/^lct/.test(modelid)) { // This is a Hue bulb limits = hueLimits; } else if (/^llc/.test(modelid)) { // This is a Living Color lamp (Bloom, Iris, etc..) limits = livingColorsLimits; } else if (/^lwb/.test(modelid)) { // This is a lux bulb limits = defaultLimits; } else { limits = defaultLimits; } } return limits; } module.exports = { convertRGBtoXY: function(rgb, modelid) { var limits = _getLimits(modelid); return _getXYStateFromRGB(rgb[0], rgb[1], rgb[2], limits); }, convertXYtoRGB: _getRGBFromXYState };