diff --git a/src/ol/color.js b/src/ol/color.js index 8ebdead1db..f8bfc08b8e 100644 --- a/src/ol/color.js +++ b/src/ol/color.js @@ -1,72 +1,183 @@ -goog.provide('ol.Color'); +// ol.color is based on goog.color and goog.color.alpha +// goog.color and goog.color.alpha use a hex string representation that encodes +// each channel as a byte (a two character hex string). This causes occasional +// loss of precision and rounding errors, especially in the alpha channel. +// FIXME don't use goog.color or goog.color.alpha +// FIXME move the color matrix code from ol.renderer.webgl.Layer to here +goog.provide('ol.color'); + +goog.require('goog.asserts'); goog.require('goog.color'); +goog.require('goog.color.alpha'); goog.require('goog.math'); - - - -/** - * @constructor - * @param {number} r Red, 0 to 255. - * @param {number} g Green, 0 to 255. - * @param {number} b Blue, 0 to 255. - * @param {number} a Alpha, 0 (fully transparent) to 1 (fully opaque). - */ -ol.Color = function(r, g, b, a) { - - /** - * @type {number} - */ - this.r = goog.math.clamp(r, 0, 255); - - /** - * @type {number} - */ - this.g = goog.math.clamp(g, 0, 255); - - /** - * @type {number} - */ - this.b = goog.math.clamp(b, 0, 255); - - /** - * @type {number} - */ - this.a = goog.math.clamp(a, 0, 1); - -}; +goog.require('goog.vec.Mat4'); /** - * @param {string} str String. - * @param {number=} opt_a Alpha. - * @return {ol.Color} Color. + * A color represented as a short array [red, green, blue, alpha]. + * red, green, and blue should be integers in the range 0..255 inclusive. + * alpha should be a float in the range 0..1 inclusive. + * @typedef {Array.} */ -ol.Color.createFromString = function(str, opt_a) { - var rgb = goog.color.hexToRgb(goog.color.parse(str).hex); - var a = goog.isDef(opt_a) ? opt_a : 1; - return new ol.Color(rgb[0], rgb[1], rgb[2], a); -}; - - -/** - * @param {ol.Color} color1 Color 1. - * @param {ol.Color} color2 Color 2. - * @return {boolean} Equals. - */ -ol.Color.equals = function(color1, color2) { - return (color1.r == color2.r && - color1.g == color2.g && - color1.b == color2.b && - color1.a == color2.a); -}; +ol.Color; /** * @param {string} s String. + * @param {ol.Color=} opt_color Color. * @return {ol.Color} Color. */ -ol.Color.parse = function(s) { - var rgb = goog.color.hexToRgb(goog.color.parse(s).hex); - return new ol.Color(rgb[0], rgb[1], rgb[2], 1); +ol.color.fromString = (function() { + + // We maintain a small cache of parsed strings. To provide cheap LRU-like + // semantics, whenever the cache grows too large we simply delete an + // arbitrary 25% of the entries. + + /** + * @const + * @type {number} + */ + var MAX_CACHE_SIZE = 1024; + + /** + * @type {Object.} + */ + var cache = {}; + + /** + * @type {number} + */ + var cacheSize = 0; + + return ( + /** + * @param {string} s String. + * @param {ol.Color=} opt_color Color. + * @return {ol.Color} Color. + */ + function(s, opt_color) { + var color; + if (cache.hasOwnProperty(s)) { + color = cache[s]; + } else { + if (cacheSize >= MAX_CACHE_SIZE) { + var i = 0; + var key; + for (key in cache) { + if (i++ & 3 === 0) { + delete cache[key]; + } + } + } + color = ol.color.fromStringInternal_(s); + cache[s] = color; + ++cacheSize; + } + return ol.color.returnOrUpdate(color, opt_color); + }); + +})(); + + +/** + * @param {string} s String. + * @private + * @return {ol.Color} Color. + */ +ol.color.fromStringInternal_ = function(s) { + + /** @preserveTry */ + try { + var rgba = goog.color.alpha.parse(s); + return goog.color.alpha.hexToRgba(rgba.hex); + } catch (e) { + // goog.color.alpha.parse throws an Error on named and rgb-style colors. + var rgb = goog.color.parse(s); + var result = goog.color.hexToRgb(rgb.hex); + result.push(1); + return result; + } +}; + + +/** + * @param {ol.Color} color Color. + * @return {boolean} Is valid. + */ +ol.color.isValid = function(color) { + return 0 <= color[0] && color[0] < 256 && + 0 <= color[1] && color[1] < 256 && + 0 <= color[2] && color[2] < 256 && + 0 <= color[3] && color[3] <= 1; +}; + + +/** + * @param {ol.Color} color Color. + * @param {ol.Color=} opt_color Color. + * @return {ol.Color} Clamped color. + */ +ol.color.normalize = function(color, opt_color) { + var result = goog.isDef(opt_color) ? opt_color : []; + result[0] = goog.math.clamp((color[0] + 0.5) | 0, 0, 255); + result[1] = goog.math.clamp((color[1] + 0.5) | 0, 0, 255); + result[2] = goog.math.clamp((color[2] + 0.5) | 0, 0, 255); + result[3] = goog.math.clamp(color[3], 0, 1); + return result; +}; + + +/** + * @param {ol.Color} color Color. + * @param {ol.Color=} opt_color Color. + * @return {ol.Color} Color. + */ +ol.color.returnOrUpdate = function(color, opt_color) { + if (goog.isDef(opt_color)) { + opt_color[0] = color[0]; + opt_color[1] = color[1]; + opt_color[2] = color[2]; + opt_color[3] = color[3]; + return opt_color; + } else { + return color; + } +}; + + +/** + * @param {ol.Color} color Color. + * @return {string} String. + */ +ol.color.toString = function(color) { + var r = color[0]; + if (r != (r | 0)) { + r = (r + 0.5) | 0; + } + var g = color[1]; + if (g != (g | 0)) { + g = (g + 0.5) | 0; + } + var b = color[2]; + if (b != (b | 0)) { + b = (b + 0.5) | 0; + } + var a = color[3]; + return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; +}; + + +/** + * @param {ol.Color} color Color. + * @param {goog.vec.Mat4.AnyType} transform Transform. + * @param {ol.Color=} opt_color Color. + * @return {ol.Color} Transformed color. + */ +ol.color.transform = function(color, transform, opt_color) { + var result = goog.isDef(opt_color) ? opt_color : []; + result = goog.vec.Mat4.multVec3(transform, color, result); + goog.asserts.assert(goog.isArray(result)); + result[3] = color[3]; + return ol.color.normalize(result, result); }; diff --git a/test/spec/ol/color.test.js b/test/spec/ol/color.test.js index f579a7f3ce..db3bfc87a9 100644 --- a/test/spec/ol/color.test.js +++ b/test/spec/ol/color.test.js @@ -1,75 +1,91 @@ -goog.provide('ol.test.Color'); +goog.provide('ol.test.color'); -describe('ol.Color', function() { - describe('constructor', function() { +describe('ol.color', function() { - it('limits r to 0-255', function() { - var c; + describe('ol.color.fromString', function() { - // legit r - c = new ol.Color(10.5, 11, 12, 0.5); - expect(c.r).to.be(10.5); - - // under r - c = new ol.Color(-10, 11, 12, 0.5); - expect(c.r).to.be(0); - - // over r - c = new ol.Color(300, 11, 12, 0.5); - expect(c.r).to.be(255); + before(function() { + sinon.spy(ol.color, 'fromStringInternal_'); }); - it('limits g to 0-255', function() { - var c; - - // legit g - c = new ol.Color(10, 11.5, 12, 0.5); - expect(c.g).to.be(11.5); - - // under g - c = new ol.Color(10, -11, 12, 0.5); - expect(c.g).to.be(0); - - // over g - c = new ol.Color(10, 275, 12, 0.5); - expect(c.g).to.be(255); + after(function() { + ol.color.fromStringInternal_.restore(); }); - it('limits b to 0-255', function() { - var c; - - // legit b - c = new ol.Color(10, 11, 12.5, 0.5); - expect(c.b).to.be(12.5); - - // under b - c = new ol.Color(10, 11, -12, 0.5); - expect(c.b).to.be(0); - - // over b - c = new ol.Color(10, 11, 500, 0.5); - expect(c.b).to.be(255); + it('can parse named colors', function() { + expect(ol.color.fromString('red')).to.eql([255, 0, 0, 1]); }); - it('limits a to 0-1', function() { - var c; + it('can parse hex colors', function() { + expect(ol.color.fromString('#00ff0080')).to.eql([0, 255, 0, 128 / 255]); + }); - // legit a - c = new ol.Color(10, 11, 12, 0.5); - expect(c.a).to.be(0.5); + it('can parse rgb colors', function() { + expect(ol.color.fromString('rgb(0, 0, 255)')).to.eql([0, 0, 255, 1]); + }); - // under a - c = new ol.Color(10, 11, 12, -0.5); - expect(c.a).to.be(0); + it('can parse rgba colors', function() { + expect(ol.color.fromString('rgba(255, 255, 0, 0.1)')).to.eql( + [255, 255, 0, 25 / 255]); + }); - // over a - c = new ol.Color(10, 11, 12, 2.5); - expect(c.a).to.be(1); + it('caches parsed values', function() { + var count = ol.color.fromStringInternal_.callCount; + ol.color.fromString('aquamarine'); + expect(ol.color.fromStringInternal_.callCount).to.be(count + 1); + ol.color.fromString('aquamarine'); + expect(ol.color.fromStringInternal_.callCount).to.be(count + 1); }); }); + describe('ol.color.isValid', function() { + + it('identifies valid colors', function() { + expect(ol.color.isValid([0, 0, 0, 0])).to.be(true); + expect(ol.color.isValid([255, 255, 255, 1])).to.be(true); + }); + + it('identifies out-of-range channels', function() { + expect(ol.color.isValid([-1, 0, 0, 0])).to.be(false); + expect(ol.color.isValid([256, 0, 0, 0])).to.be(false); + expect(ol.color.isValid([0, -1, 0, 0])).to.be(false); + expect(ol.color.isValid([0, 256, 0, 0])).to.be(false); + expect(ol.color.isValid([0, 0, -1, 0])).to.be(false); + expect(ol.color.isValid([0, 0, 256, 0])).to.be(false); + expect(ol.color.isValid([0, 0, -1, 0])).to.be(false); + expect(ol.color.isValid([0, 0, 256, 0])).to.be(false); + expect(ol.color.isValid([0, 0, 0, -1])).to.be(false); + expect(ol.color.isValid([0, 0, 0, 2])).to.be(false); + }); + + }); + + describe('ol.color.normalize', function() { + + it('clamps out-of-range channels', function() { + expect(ol.color.normalize([-1, 256, 0, 2])).to.eql([0, 255, 0, 1]); + }); + + it('rounds color channels to integers', function() { + expect(ol.color.normalize([1.2, 2.5, 3.7, 1])).to.eql([1, 3, 4, 1]); + }); + + }); + + describe('ol.color.toString', function() { + + it('converts valid colors', function() { + expect(ol.color.toString([1, 2, 3, 0.4])).to.be('rgba(1,2,3,0.4)'); + }); + + it('rounds to integers if needed', function() { + expect(ol.color.toString([1.2, 2.5, 3.7, 0.4])).to.be('rgba(1,3,4,0.4)'); + }); + + }); }); -goog.require('ol.Color'); + +goog.require('ol.color');