From 4d97b583c678ce4874c610de4fe75d0f2cdccf5e Mon Sep 17 00:00:00 2001 From: tschaub Date: Thu, 25 Oct 2012 16:28:17 -0600 Subject: [PATCH] Match the current Filter Effects spec This commit is a cherry-pick of 19f7778. The current draft of the [filter spec](https://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/index.html) describes brightness, contrast, hue-rotate, and saturate functions that roughly match our layer's setBrightness, setContrast, setHue, and setSaturation methods. These changes make the range of our methods match the corresponding functions in the spec. The one exception is the brightness function. The spec says it has a range of 0 to positive infinity. The WebKit implementation accepts a range of -1 to 1 (as of https://github.com/WebKit/webkit/commit/8f4765e569). There's an open (ticket)[https://www.w3.org/Bugs/Public/show_bug.cgi?id=15647] recommending that the spec be changed to match. I'm not stuck on having our methods match those of the filter spec, but the parity would be nice. These changes leave the WebGL map renderer "broken" (whacky colors). It would be straightforward to update the current fragment shader to handle the new range of hue, but the brightness, contrast, and saturation handling will need to be reworked. For inspiration, here are the color transformation matrix calculations the WebKit filters: https://github.com/WebKit/webkit/blob/8f4765e5698c9171c2b3f984d0d9d65188185de3/Source/WebCore/platform/graphics/chromium/cc/CCRenderSurfaceFilters.cpp#L64-80 Conflicts: src/ol/renderer/dom/domlayerrenderer.js --- examples/side-by-side.js | 14 +- src/ol/layer/layer.js | 36 +++- test/ol.html | 1 + test/spec/ol/layer/layer.test.js | 275 +++++++++++++++++++++++++++++++ 4 files changed, 316 insertions(+), 10 deletions(-) create mode 100644 test/spec/ol/layer/layer.test.js diff --git a/examples/side-by-side.js b/examples/side-by-side.js index 5d7f531d31..e0ee1b8827 100644 --- a/examples/side-by-side.js +++ b/examples/side-by-side.js @@ -56,9 +56,9 @@ webglMap.getControls().push(new ol.control.MousePosition({ var keyboardInteraction = new ol.interaction.Keyboard(); keyboardInteraction.addCallback('0', function() { layer.setBrightness(0); - layer.setContrast(0); + layer.setContrast(1); layer.setHue(0); - layer.setSaturation(0); + layer.setSaturation(1); layer.setOpacity(1); layer.setVisible(true); }); @@ -72,13 +72,14 @@ keyboardInteraction.addCallback('c', function() { layer.setContrast(layer.getContrast() - 0.1); }); keyboardInteraction.addCallback('C', function() { - layer.setContrast(layer.getContrast() + 0.1); + // contrast is unbounded, but for this example we clamp to 3 + layer.setContrast(Math.min(3, layer.getContrast() + 0.1)); }); keyboardInteraction.addCallback('h', function() { - layer.setHue(layer.getHue() - 0.1); + layer.setHue(layer.getHue() - 10); }); keyboardInteraction.addCallback('H', function() { - layer.setHue(layer.getHue() + 0.1); + layer.setHue(layer.getHue() + 10); }); keyboardInteraction.addCallback('o', function() { layer.setOpacity(layer.getOpacity() - 0.1); @@ -93,7 +94,8 @@ keyboardInteraction.addCallback('s', function() { layer.setSaturation(layer.getSaturation() - 0.1); }); keyboardInteraction.addCallback('S', function() { - layer.setSaturation(layer.getSaturation() + 0.1); + // saturation is unbounded, but for this example we clamp to 3 + layer.setSaturation(Math.min(3, layer.getSaturation() + 0.1)); }); keyboardInteraction.addCallback('vV', function() { layer.setVisible(!layer.getVisible()); diff --git a/src/ol/layer/layer.js b/src/ol/layer/layer.js index 0334880fad..c91471b747 100644 --- a/src/ol/layer/layer.js +++ b/src/ol/layer/layer.js @@ -40,13 +40,13 @@ ol.layer.Layer = function(layerOptions) { this.setBrightness( goog.isDef(layerOptions.brightness) ? layerOptions.brightness : 0); this.setContrast( - goog.isDef(layerOptions.contrast) ? layerOptions.contrast : 0); + goog.isDef(layerOptions.contrast) ? layerOptions.contrast : 1); this.setHue( goog.isDef(layerOptions.hue) ? layerOptions.hue : 0); this.setOpacity( goog.isDef(layerOptions.opacity) ? layerOptions.opacity : 1); this.setSaturation( - goog.isDef(layerOptions.saturation) ? layerOptions.saturation : 0); + goog.isDef(layerOptions.saturation) ? layerOptions.saturation : 1); this.setVisible( goog.isDef(layerOptions.visible) ? layerOptions.visible : true); @@ -164,6 +164,23 @@ ol.layer.Layer.prototype.isReady = function() { /** + * Adjust the layer brightness. A value of -1 will render the layer completely + * black. A value of 0 will leave the brightness unchanged. A value of 1 will + * render the layer completely white. Other values are linear multipliers on + * the effect (values are clamped between -1 and 1). + * + * The filter effects draft [1] says the brightness function is supposed to + * render 0 black, 1 unchanged, and all other values as a linear multiplier. + * + * The current WebKit implementation clamps values between -1 (black) and 1 + * (white) [2]. There is a bug open to change the filter effect spec [3]. + * + * TODO: revisit this if the spec is still unmodified before we release + * + * [1] https://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/index.html + * [2] https://github.com/WebKit/webkit/commit/8f4765e569 + * [3] https://www.w3.org/Bugs/Public/show_bug.cgi?id=15647 + * * @param {number} brightness Brightness. */ ol.layer.Layer.prototype.setBrightness = function(brightness) { @@ -179,10 +196,14 @@ goog.exportProperty( /** + * Adjust the layer contrast. A value of 0 will render the layer completely + * grey. A value of 1 will leave the contrast unchanged. Other values are + * linear multipliers on the effect (and values over 1 are permitted). + * * @param {number} contrast Contrast. */ ol.layer.Layer.prototype.setContrast = function(contrast) { - contrast = goog.math.clamp(contrast, -1, 1); + contrast = Math.max(0, contrast); if (contrast != this.getContrast()) { this.set(ol.layer.LayerProperty.CONTRAST, contrast); } @@ -194,6 +215,8 @@ goog.exportProperty( /** + * Apply a hue-rotation to the layer. A value of 0 will leave the hue + * unchanged. Other values are degrees around the color circle. * @param {number} hue Hue. */ ol.layer.Layer.prototype.setHue = function(hue) { @@ -223,10 +246,15 @@ goog.exportProperty( /** + * Adjust layer saturation. A value of 0 will render the layer completely + * unsaturated. A value of 1 will leave the saturation unchanged. Other + * values are linear multipliers of the effect (and values over 1 are + * permitted). + * * @param {number} saturation Saturation. */ ol.layer.Layer.prototype.setSaturation = function(saturation) { - saturation = goog.math.clamp(saturation, -1, 1); + saturation = Math.max(0, saturation); if (saturation != this.getSaturation()) { this.set(ol.layer.LayerProperty.SATURATION, saturation); } diff --git a/test/ol.html b/test/ol.html index 1d438b53cd..08793ee015 100644 --- a/test/ol.html +++ b/test/ol.html @@ -78,6 +78,7 @@ + diff --git a/test/spec/ol/layer/layer.test.js b/test/spec/ol/layer/layer.test.js new file mode 100644 index 0000000000..00988c7a09 --- /dev/null +++ b/test/spec/ol/layer/layer.test.js @@ -0,0 +1,275 @@ +describe('ol.layer.Layer', function() { + + describe('constructor (defaults)', function() { + + var layer; + + beforeEach(function() { + layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }) + }); + }); + + afterEach(function() { + layer.dispose(); + }); + + it('creates an instance', function() { + expect(layer).toBeA(ol.layer.Layer); + }); + + it('provides default brightness', function() { + expect(layer.getBrightness()).toBe(0); + }); + + it('provides default contrast', function() { + expect(layer.getContrast()).toBe(1); + }); + + it('provides default hue', function() { + expect(layer.getHue()).toBe(0); + }); + + it('provides default opacity', function() { + expect(layer.getOpacity()).toBe(1); + }); + + it('provides default saturation', function() { + expect(layer.getSaturation()).toBe(1); + }); + + it('provides default visibility', function() { + expect(layer.getVisible()).toBe(true); + }); + + }); + + describe('constructor (options)', function() { + + it('accepts options', function() { + var layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }), + brightness: 0.5, + contrast: 10, + hue: 180, + opacity: 0.5, + saturation: 5, + visible: false + }); + + expect(layer.getBrightness()).toBe(0.5); + expect(layer.getContrast()).toBe(10); + expect(layer.getHue()).toBe(180); + expect(layer.getOpacity()).toBe(0.5); + expect(layer.getSaturation()).toBe(5); + expect(layer.getVisible()).toBe(false); + + layer.dispose(); + }); + + }); + + describe('#setBrightness', function() { + + var layer; + + beforeEach(function() { + layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }) + }); + }); + + afterEach(function() { + layer.dispose(); + }); + + it('accepts a positive number', function() { + layer.setBrightness(0.3); + expect(layer.getBrightness()).toBe(0.3); + }); + + it('accepts a negative number', function() { + layer.setBrightness(-0.7); + expect(layer.getBrightness()).toBe(-0.7); + }); + + it('clamps to 1', function() { + layer.setBrightness(1.5); + expect(layer.getBrightness()).toBe(1); + }); + + it('clamps to -1', function() { + layer.setBrightness(-3); + expect(layer.getBrightness()).toBe(-1); + }); + + }); + + describe('#setContrast', function() { + + var layer; + + beforeEach(function() { + layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }) + }); + }); + + afterEach(function() { + layer.dispose(); + }); + + it('accepts a small positive number', function() { + layer.setContrast(0.3); + expect(layer.getContrast()).toBe(0.3); + }); + + it('clamps to 0', function() { + layer.setContrast(-0.7); + expect(layer.getContrast()).toBe(0); + }); + + it('accepts a big positive number', function() { + layer.setContrast(42); + expect(layer.getContrast()).toBe(42); + }); + + }); + + + describe('#setHue', function() { + + var layer; + + beforeEach(function() { + layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }) + }); + }); + + afterEach(function() { + layer.dispose(); + }); + + it('accepts a small positive number', function() { + layer.setHue(0.3); + expect(layer.getHue()).toBe(0.3); + }); + + it('accepts a small negative number', function() { + layer.setHue(-0.7); + expect(layer.getHue()).toBe(-0.7); + }); + + it('accepts a big positive number', function() { + layer.setHue(42); + expect(layer.getHue()).toBe(42); + }); + + it('accepts a big negative number', function() { + layer.setHue(-100); + expect(layer.getHue()).toBe(-100); + }); + + }); + + + describe('#setOpacity', function() { + + var layer; + + beforeEach(function() { + layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }) + }); + }); + + afterEach(function() { + layer.dispose(); + }); + + it('accepts a positive number', function() { + layer.setOpacity(0.3); + expect(layer.getOpacity()).toBe(0.3); + }); + + it('clamps to 0', function() { + layer.setOpacity(-1.5); + expect(layer.getOpacity()).toBe(0); + }); + + it('clamps to 1', function() { + layer.setOpacity(3); + expect(layer.getOpacity()).toBe(1); + }); + + }); + + + describe('#setSaturation', function() { + + var layer; + + beforeEach(function() { + layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }) + }); + }); + + afterEach(function() { + layer.dispose(); + }); + + it('accepts a small positive number', function() { + layer.setSaturation(0.3); + expect(layer.getSaturation()).toBe(0.3); + }); + + it('clamps to 0', function() { + layer.setSaturation(-0.7); + expect(layer.getSaturation()).toBe(0); + }); + + it('accepts a big positive number', function() { + layer.setSaturation(42); + expect(layer.getSaturation()).toBe(42); + }); + + }); + + + describe('#setVisible', function() { + + it('sets visible property', function() { + var layer = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: ol.Projection.getFromCode('EPSG:4326') + }) + }); + + layer.setVisible(false); + expect(layer.getVisible()).toBe(false); + + layer.setVisible(true); + expect(layer.getVisible()).toBe(true); + + layer.dispose(); + }); + + }); + +});