diff --git a/src/style-spec/function/index.js b/src/style-spec/function/index.js index 084df7731f7..09b13b5882e 100644 --- a/src/style-spec/function/index.js +++ b/src/style-spec/function/index.js @@ -37,6 +37,10 @@ function createFunction(parameters, propertySpec) { } } + if (parameters.colorSpace && parameters.colorSpace !== 'rgb' && !colorSpaces[parameters.colorSpace]) { + throw new Error(`Unknown color space: ${parameters.colorSpace}`); + } + let innerFun; let hashedStops; let categoricalKeyType; @@ -62,31 +66,6 @@ function createFunction(parameters, propertySpec) { throw new Error(`Unknown function type "${type}"`); } - let outputFunction; - - // If we're interpolating colors in a color system other than RGBA, - // first translate all stop values to that color system, then interpolate - // arrays as usual. The `outputFunction` option lets us then translate - // the result of that interpolation back into RGBA. - if (parameters.colorSpace && parameters.colorSpace !== 'rgb') { - if (colorSpaces[parameters.colorSpace]) { - const colorspace = colorSpaces[parameters.colorSpace]; - // Avoid mutating the parameters value - parameters = JSON.parse(JSON.stringify(parameters)); - for (let s = 0; s < parameters.stops.length; s++) { - parameters.stops[s] = [ - parameters.stops[s][0], - colorspace.forward(parameters.stops[s][1]) - ]; - } - outputFunction = colorspace.reverse; - } else { - throw new Error(`Unknown color space: ${parameters.colorSpace}`); - } - } else { - outputFunction = identityFunction; - } - if (zoomAndFeatureDependent) { const featureFunctions = {}; const zoomStops = []; @@ -116,10 +95,10 @@ function createFunction(parameters, propertySpec) { interpolationFactor: Interpolate.interpolationFactor.bind(undefined, {name: 'linear'}), zoomStops: featureFunctionStops.map(s => s[0]), evaluate({zoom}, properties) { - return outputFunction(evaluateExponentialFunction({ + return evaluateExponentialFunction({ stops: featureFunctionStops, base: parameters.base - }, propertySpec, zoom).evaluate(zoom, properties)); + }, propertySpec, zoom).evaluate(zoom, properties); } }; } else if (zoomDependent) { @@ -129,7 +108,7 @@ function createFunction(parameters, propertySpec) { Interpolate.interpolationFactor.bind(undefined, {name: 'exponential', base: parameters.base !== undefined ? parameters.base : 1}) : () => 0, zoomStops: parameters.stops.map(s => s[0]), - evaluate: ({zoom}) => outputFunction(innerFun(parameters, propertySpec, zoom, hashedStops, categoricalKeyType)) + evaluate: ({zoom}) => innerFun(parameters, propertySpec, zoom, hashedStops, categoricalKeyType) }; } else { return { @@ -139,7 +118,7 @@ function createFunction(parameters, propertySpec) { if (value === undefined) { return coalesce(parameters.default, propertySpec.default); } - return outputFunction(innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType)); + return innerFun(parameters, propertySpec, value, hashedStops, categoricalKeyType); } }; } @@ -187,7 +166,12 @@ function evaluateExponentialFunction(parameters, propertySpec, input) { const outputLower = parameters.stops[index][1]; const outputUpper = parameters.stops[index + 1][1]; - const interp = interpolate[propertySpec.type] || identityFunction; + let interp = interpolate[propertySpec.type] || identityFunction; + + if (parameters.colorSpace && parameters.colorSpace !== 'rgb') { + const colorspace = colorSpaces[parameters.colorSpace]; + interp = (a, b) => colorspace.reverse(colorspace.interpolate(colorspace.forward(a), colorspace.forward(b), t)); + } if (typeof outputLower.evaluate === 'function') { return { diff --git a/src/style-spec/util/color_spaces.js b/src/style-spec/util/color_spaces.js index 17ac271cbb7..3578d6fb151 100644 --- a/src/style-spec/util/color_spaces.js +++ b/src/style-spec/util/color_spaces.js @@ -1,6 +1,7 @@ // @flow const Color = require('./color'); +const interpolateNumber = require('./interpolate').number; type LABColor = { l: number, @@ -77,6 +78,15 @@ function labToRgb(labColor: LABColor): Color { ); } +function interpolateLab(from: LABColor, to: LABColor, t: number) { + return { + l: interpolateNumber(from.l, to.l, t), + a: interpolateNumber(from.a, to.a, t), + b: interpolateNumber(from.b, to.b, t), + alpha: interpolateNumber(from.alpha, to.alpha, t) + }; +} + // HCL function rgbToHcl(rgbColor: Color): HCLColor { const {l, a, b} = rgbToLab(rgbColor); @@ -101,13 +111,29 @@ function hclToRgb(hclColor: HCLColor): Color { }); } +function interpolateHue(a: number, b: number, t: number) { + const d = b - a; + return a + t * (d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d); +} + +function interpolateHcl(from: HCLColor, to: HCLColor, t: number) { + return { + h: interpolateHue(from.h, to.h, t), + c: interpolateNumber(from.c, to.c, t), + l: interpolateNumber(from.l, to.l, t), + alpha: interpolateNumber(from.alpha, to.alpha, t) + }; +} + module.exports = { lab: { forward: rgbToLab, - reverse: labToRgb + reverse: labToRgb, + interpolate: interpolateLab }, hcl: { forward: rgbToHcl, - reverse: hclToRgb + reverse: hclToRgb, + interpolate: interpolateHcl } }; diff --git a/test/integration/render-tests/background-color/colorSpace-hcl/expected.png b/test/integration/render-tests/background-color/colorSpace-hcl/expected.png new file mode 100644 index 00000000000..3ef07516e2a Binary files /dev/null and b/test/integration/render-tests/background-color/colorSpace-hcl/expected.png differ diff --git a/test/integration/render-tests/background-color/colorSpace-hcl/style.json b/test/integration/render-tests/background-color/colorSpace-hcl/style.json new file mode 100644 index 00000000000..00facfba21b --- /dev/null +++ b/test/integration/render-tests/background-color/colorSpace-hcl/style.json @@ -0,0 +1,32 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "zoom": 5, + "sources": {}, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": { + "stops": [ + [ + 0, + "rgb(118,0,118)" + ], + [ + 10, + "rgb(255,155,0)" + ] + ], + "colorSpace": "hcl" + } + } + } + ] +} diff --git a/test/integration/render-tests/background-color/colorSpace-lab/expected.png b/test/integration/render-tests/background-color/colorSpace-lab/expected.png new file mode 100644 index 00000000000..323d32a43a0 Binary files /dev/null and b/test/integration/render-tests/background-color/colorSpace-lab/expected.png differ diff --git a/test/integration/render-tests/background-color/colorSpace-lab/style.json b/test/integration/render-tests/background-color/colorSpace-lab/style.json new file mode 100644 index 00000000000..edd2111d47a --- /dev/null +++ b/test/integration/render-tests/background-color/colorSpace-lab/style.json @@ -0,0 +1,32 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "zoom": 5, + "sources": {}, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": { + "stops": [ + [ + 0, + "rgb(118,0,118)" + ], + [ + 10, + "rgb(255,155,0)" + ] + ], + "colorSpace": "lab" + } + } + } + ] +} diff --git a/test/unit/style-spec/function.test.js b/test/unit/style-spec/function.test.js index 84feffa105c..757cb229b68 100644 --- a/test/unit/style-spec/function.test.js +++ b/test/unit/style-spec/function.test.js @@ -184,40 +184,38 @@ test('exponential function', (t) => { t.end(); }); - t.test('lab colorspace', {skip: true}, (t) => { + t.test('lab colorspace', (t) => { const f = createFunction({ type: 'exponential', colorSpace: 'lab', - stops: [[1, [0, 0, 0, 1]], [10, [0, 1, 1, 1]]] + stops: [[1, 'rgba(0,0,0,1)'], [10, 'rgba(0,255,255,1)']] }, { type: 'color' }).evaluate; t.deepEqual(f({zoom: 0}), new Color(0, 0, 0, 1)); - t.deepEqual(f({zoom: 5}).map((n) => { - return parseFloat(n.toFixed(3)); - }), new Color(0, 0.444, 0.444, 1)); + t.equalWithPrecision(f({zoom: 5}).r, 0, 1e-6); + t.equalWithPrecision(f({zoom: 5}).g, 0.444, 1e-3); + t.equalWithPrecision(f({zoom: 5}).b, 0.444, 1e-3); t.end(); }); - t.test('rgb colorspace', {skip: true}, (t) => { + t.test('rgb colorspace', (t) => { const f = createFunction({ type: 'exponential', colorSpace: 'rgb', - stops: [[0, [0, 0, 0, 1]], [10, [1, 1, 1, 1]]] + stops: [[0, 'rgba(0,0,0,1)'], [10, 'rgba(255,255,255,1)']] }, { type: 'color' }).evaluate; - t.deepEqual(f({zoom: 5}).map((n) => { - return parseFloat(n.toFixed(3)); - }), new Color(0.5, 0.5, 0.5, 1)); + t.deepEqual(f({zoom: 5}), new Color(0.5, 0.5, 0.5, 1)); t.end(); }); - t.test('unknown color spaces', {skip: true}, (t) => { + t.test('unknown color spaces', (t) => { t.throws(() => { createFunction({ type: 'exponential', @@ -231,7 +229,7 @@ test('exponential function', (t) => { t.end(); }); - t.test('interpolation mutation avoidance', {skip: true}, (t) => { + t.test('interpolation mutation avoidance', (t) => { const params = { type: 'exponential', colorSpace: 'lab', @@ -289,7 +287,7 @@ test('exponential function', (t) => { t.end(); }); - t.test('property type mismatch, function default', {skip: true}, (t) => { + t.test('property type mismatch, function default', (t) => { const f = createFunction({ property: 'foo', type: 'exponential', @@ -304,7 +302,7 @@ test('exponential function', (t) => { t.end(); }); - t.test('property type mismatch, spec default', {skip: true}, (t) => { + t.test('property type mismatch, spec default', (t) => { const f = createFunction({ property: 'foo', type: 'exponential', @@ -818,7 +816,7 @@ test('identity function', (t) => { t.end(); }); - t.test('number function default', {skip: true}, (t) => { + t.test('number function default', (t) => { const f = createFunction({ property: 'foo', type: 'identity', @@ -832,7 +830,7 @@ test('identity function', (t) => { t.end(); }); - t.test('number spec default', {skip: true}, (t) => { + t.test('number spec default', (t) => { const f = createFunction({ property: 'foo', type: 'identity'