diff --git a/src/scripts/components/color.js b/src/scripts/components/color.js index 0420ca7..fcc1228 100644 --- a/src/scripts/components/color.js +++ b/src/scripts/components/color.js @@ -10,6 +10,8 @@ module.exports = { description: '' }, initialize: function(minified, clay) { + var HTML = minified.HTML; + var self = this; /** * @param {string|boolean|number} color @@ -22,13 +24,23 @@ module.exports = { return 'transparent'; } + color = padColorString(color); + + return '#' + (useSunlight ? sunlightColorMap[color] : color); + } + + /** + * @param {string} color + * @return {string} + */ + function padColorString(color) { color = color.toLowerCase(); while (color.length < 6) { color = '0' + color; } - return '#' + (useSunlight ? sunlightColorMap[color] : color); + return color; } /** @@ -37,12 +49,66 @@ module.exports = { */ function normalizeColor(value) { switch (typeof value) { - case 'number': return value.toString(16); + case 'number': return padColorString(value.toString(16)); case 'string': return value.replace(/^#|^0x/, ''); default: return value; } } + /** + * @param {Array.} layout + * @returns {Array} + */ + function flattenLayout(layout) { + return layout.reduce(function(a, b) { + return a.concat(b); + }, []); + } + + /** + * Convert HEX color to LAB. + * Adapted from: https://github.com/antimatter15/rgb-lab + * @param {string} hex + * @returns {Array} - [l, a, b] + */ + function hex2lab(hex) { + hex = hex.replace(/^#|^0x/, ''); + + var r = parseInt(hex.slice(0, 2), 16) / 255; + var g = parseInt(hex.slice(2, 4), 16) / 255; + var b = parseInt(hex.slice(4), 16) / 255; + + r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; + g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; + b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; + + var x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047; + var y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000; + var z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883; + + x = (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116; + y = (y > 0.008856) ? Math.pow(y, 1 / 3) : (7.787 * y) + 16 / 116; + z = (z > 0.008856) ? Math.pow(z, 1 / 3) : (7.787 * z) + 16 / 116; + + return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]; + } + + /** + * Find the perceptual color distance between two LAB colors + * @param {Array} labA + * @param {Array} labB + * @returns {number} + */ + function deltaE(labA, labB) { + var deltaL = labA[0] - labB[0]; + var deltaA = labA[1] - labB[1]; + var deltaB = labA[2] - labB[2]; + + return Math.sqrt(Math.pow(deltaL, 2) + + Math.pow(deltaA, 2) + + Math.pow(deltaB, 2)); + } + /** * Returns the layout based on the connected watch * @returns {Array} @@ -62,8 +128,32 @@ module.exports = { return standardLayouts.COLOR; } - var HTML = minified.HTML; - var self = this; + /** + * @param {number|string} color + * @return {number} + */ + self.roundColorToLayout = function(color) { + var itemValue = normalizeColor(color); + + // if the color is not in the layout we will need find the closest match + if (colorList.indexOf(itemValue) === -1) { + var itemValueLAB = hex2lab(itemValue); + var differenceArr = colorList.map(function(color) { + var colorLAB = hex2lab(normalizeColor(color)); + return deltaE(itemValueLAB, colorLAB); + }); + + // Get the lowest number from the differenceArray + var lowest = Math.min.apply(Math, differenceArr); + + // Get the index for that lowest number + var index = differenceArr.indexOf(lowest); + itemValue = colorList[index]; + } + + return parseInt(itemValue, 16); + }; + var useSunlight = self.config.sunlight !== false; var sunlightColorMap = { '000000': '000000', '000055': '001e41', '0000aa': '004387', '0000ff': '0068ca', @@ -118,6 +208,12 @@ module.exports = { layout = [layout]; } + var colorList = flattenLayout(layout).map(function(item) { + return normalizeColor(item); + }).filter(function(item) { + return item; + }); + var grid = ''; var rows = layout.length; var cols = 0; diff --git a/src/scripts/lib/manipulators.js b/src/scripts/lib/manipulators.js index f4798a1..84bc11e 100755 --- a/src/scripts/lib/manipulators.js +++ b/src/scripts/lib/manipulators.js @@ -145,16 +145,12 @@ module.exports = { }, color: { get: function() { - return parseInt(this.$manipulatorTarget.get('value'), 16); + return parseInt(this.$manipulatorTarget.get('value'), 10) || 0; }, set: function(value) { - switch (typeof value) { - case 'number': value = value.toString(16); break; - case 'string': value = value.replace(/^#|^0x/, ''); break; - } - value = value || '000000'; + value = this.roundColorToLayout(value || 0); - if (this.get() === parseInt(value, 16)) { return this; } + if (this.get() === value) { return this; } this.$manipulatorTarget.set('value', value); return this.trigger('change'); }, diff --git a/test/spec/components/color.js b/test/spec/components/color.js index 620e50f..21650e5 100644 --- a/test/spec/components/color.js +++ b/test/spec/components/color.js @@ -134,6 +134,39 @@ function testAutoLayout(platform, layout, allowGray, desc, activeWatchInfo) { }); } +/** + * @param {number|string} input + * @param {number} expected + * @param {array|string} layout + * @return {void} + */ +function testClosestToLayout(input, expected, layout) { + it('rounds ' + input + ' to ' + expected.toString(16) + + ' in layout: ' + JSON.stringify(layout), + function() { + var clayConfig; + var colorItem; + + clayConfig = fixture.clayConfig([{ + type: 'color', + sunlight: false, + layout: layout + }]); + colorItem = clayConfig.getItemsByType('color')[0]; + colorItem.set(input); + assert.strictEqual(colorItem.get(), expected); + + clayConfig = fixture.clayConfig([{ + type: 'color', + sunlight: false, + layout: layout + }]); + colorItem = clayConfig.getItemsByType('color')[0]; + colorItem.set(input); + assert.strictEqual(colorItem.get(), expected); + }); +} + /** * @param {boolean} sunlight * @param {Array|string} [layout=color] @@ -291,6 +324,41 @@ describe('component - color', function() { assert.strictEqual($picker.get('classList').contains('show'), false); }); + describe('color rounding', function() { + var layout = 'GRAY'; + testClosestToLayout('000000', 0x000000, layout); + testClosestToLayout('005500', 0x000000, layout); + testClosestToLayout('55aa55', 0xaaaaaa, layout); + testClosestToLayout('ffaaff', 0xaaaaaa, layout); + testClosestToLayout('ffffaa', 0xffffff, layout); + testClosestToLayout('ffffff', 0xffffff, layout); + + layout = 'BLACK_WHITE'; + testClosestToLayout('000000', 0x000000, layout); + testClosestToLayout('005500', 0x000000, layout); + testClosestToLayout('55aa55', 0xffffff, layout); + testClosestToLayout('ffaaff', 0xffffff, layout); + testClosestToLayout('ffffaa', 0xffffff, layout); + testClosestToLayout('ffffff', 0xffffff, layout); + + layout = 'COLOR'; + testClosestToLayout('000000', 0x000000, layout); + testClosestToLayout('005500', 0x005500, layout); + testClosestToLayout('55bb55', 0x55aa55, layout); + testClosestToLayout('ff99ff', 0xffaaff, layout); + testClosestToLayout('ffffee', 0xffffff, layout); + testClosestToLayout('ffffff', 0xffffff, layout); + + layout = ['ff0000', 'ff5500', 'ff00ff', '00ff00', '555555']; + testClosestToLayout('000000', 0x555555, layout); + testClosestToLayout('ff0000', 0xff0000, layout); + testClosestToLayout('ff1111', 0xff0000, layout); + testClosestToLayout('00aa00', 0x00ff00, layout); + testClosestToLayout('ff55ff', 0xff00ff, layout); + testClosestToLayout('aaaaaa', 0x555555, layout); + testClosestToLayout('ffffff', 0x555555, layout); + }); + describe('layouts', function() { // allow gray does nothing for 2.x