From 85c3aae454fdb23a34335ed7c386f5d469d144d5 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Tue, 22 Oct 2019 18:13:41 +0200 Subject: [PATCH] ShaderBuilder / add support for color interpolation in parse --- src/ol/webgl/ShaderBuilder.js | 35 ++++++++++++++++++------ test/spec/ol/webgl/shaderbuilder.test.js | 28 +++++++++++++++++-- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index 5d2e74ba2a..15e74d736b 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -50,7 +50,7 @@ export function formatColor(color) { * Possible inferred types from a given value or expression * @enum {number} */ -const ValueTypes = { +export const ValueTypes = { UNKNOWN: -1, NUMBER: 0, STRING: 1, @@ -163,7 +163,7 @@ export function check(value) { } // check operator arguments - if (Array.isArray(value)) { + if (Array.isArray(value) && typeof value[0] === 'string') { switch (value[0]) { case 'get': case 'var': @@ -226,22 +226,35 @@ export function check(value) { * For attributes, a prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in * the final assignment string (depending on whether we're outputting a vertex or fragment shader). * + * If a wrong value type is supplied to an operator (i. e. using colors with the `clamp` operator), an exception + * will be thrown. + * + * Note that by default, the `string` value type will be given precedence over `color`, so for example the + * `'yellow'` literal value will be parsed as a `string` while being a valid CSS color. This can be changed with + * the `typeHint` optional parameter which disambiguates what kind of value is expected. + * * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. * @param {Array} attributes Array containing the attribute names **without a prefix**; * it is passed along recursively * @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`). * @param {Array} variables Array containing the variable names **without a prefix**; * it is passed along recursively + * @param {ValueTypes} [typeHint] Hint for inferred type * @returns {string} Assignment string. */ -export function parse(value, attributes, attributePrefix, variables) { +export function parse(value, attributes, attributePrefix, variables, typeHint) { check(value); const v = value; function p(value) { return parse(value, attributes, attributePrefix, variables); } - if (Array.isArray(v)) { + function pC(value) { + return parse(value, attributes, attributePrefix, variables, ValueTypes.COLOR); + } + + // operator + if (Array.isArray(v) && typeof value[0] === 'string') { switch (v[0]) { // reading operators case 'get': @@ -263,6 +276,10 @@ export function parse(value, attributes, attributePrefix, variables) { case 'clamp': return `clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])})`; case 'stretch': return `(clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])}) * ((${p(v[5])} - ${p(v[4])}) / (${p(v[3])} - ${p(v[2])})) + ${p(v[4])})`; + // color operators + case 'interpolate': + return `mix(${pC(v[2])}, ${pC(v[3])}, ${p(v[1])})`; + // logical operators case '>': case '>=': @@ -275,12 +292,14 @@ export function parse(value, attributes, attributePrefix, variables) { case 'between': return `(${p(v[1])} >= ${p(v[2])} && ${p(v[1])} <= ${p(v[3])} ? 1.0 : 0.0)`; - default: throw new Error('Unrecognized literal style expression: ' + JSON.stringify(value)); + default: throw new Error('Invalid style expression: ' + JSON.stringify(value)); } - } else if (typeof value === 'number') { - return formatNumber(value); + } else if (isValueTypeNumber(value)) { + return formatNumber(/** @type {number} */(value)); + } else if (isValueTypeString(value) && (typeHint === undefined || typeHint == ValueTypes.STRING)) { + return `"${value}"`; } else { - throw new Error('Invalid value type in expression: ' + JSON.stringify(value)); + return formatColor(/** @type {number[]|string} */(value)); } } diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index 4288b1f710..a9d339c59b 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -8,7 +8,8 @@ import { isValueTypeString, parse, parseLiteralStyle, - ShaderBuilder + ShaderBuilder, + ValueTypes } from '../../../../src/ol/webgl/ShaderBuilder.js'; describe('ol.webgl.ShaderBuilder', function() { @@ -270,6 +271,10 @@ void main(void) { it('does not throw on valid expressions', function(done) { check(1); + check('attr'); + check('rgba(12, 34, 56, 0.5)'); + check([255, 255, 255, 1]); + check([255, 255, 255]); check(['get', 'myAttr']); check(['var', 'myValue']); check(['time']); @@ -283,6 +288,8 @@ void main(void) { check(['==', 10, ['get', 'attr4']]); check(['between', ['get', 'attr4'], -4.0, 5.0]); check(['!', ['get', 'attr4']]); + check(['interpolate', ['get', 'attr4'], 'green', '#3344FF']); + check(['interpolate', 0.2, [10, 20, 30], [255, 255, 255, 1]]); done(); }); @@ -303,6 +310,11 @@ void main(void) { } catch (e) { thrown = true; } + try { + check(['interpolate', ['get', 'attr4'], 1, '#3344FF']); + } catch (e) { + thrown = true; + } expect(thrown).to.be(true); }); @@ -355,13 +367,16 @@ void main(void) { attributes = []; variables = []; prefix = 'a_'; - parseFn = function(value) { - return parse(value, attributes, prefix, variables); + parseFn = function(value, type) { + return parse(value, attributes, prefix, variables, type); }; }); it('parses expressions & literal values', function() { expect(parseFn(1)).to.eql('1.0'); + expect(parseFn('a_random_string')).to.eql('"a_random_string"'); + expect(parseFn([255, 127.5, 63.75, 0.1])).to.eql('vec4(1.0, 0.5, 0.25, 0.1)'); + expect(parseFn([255, 127.5, 63.75])).to.eql('vec4(1.0, 0.5, 0.25, 1.0)'); expect(parseFn(['get', 'myAttr'])).to.eql('a_myAttr'); expect(parseFn(['var', 'myValue'])).to.eql('u_myValue'); expect(parseFn(['time'])).to.eql('u_time'); @@ -375,10 +390,17 @@ void main(void) { expect(parseFn(['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4 ? 1.0 : 0.0)'); expect(parseFn(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0 ? 1.0 : 0.0)'); expect(parseFn(['!', ['get', 'attr4']])).to.eql('(a_attr4 > 0.0 ? 0.0 : 1.0)'); + expect(parseFn(['interpolate', ['get', 'attr4'], [255, 255, 255, 1], 'transparent'])).to.eql( + 'mix(vec4(1.0, 1.0, 1.0, 1.0), vec4(0.0, 0.0, 0.0, 0.0), a_attr4)'); expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3', 'attr4']); expect(variables).to.eql(['myValue']); }); + it('gives precedence to the string type unless asked otherwise', function() { + expect(parseFn('lightgreen')).to.eql('"lightgreen"'); + expect(parseFn('lightgreen', ValueTypes.COLOR)).to.eql('vec4(0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1.0)'); + }); + it('does not register an attribute several times', function() { parseFn(['get', 'myAttr']); parseFn(['var', 'myVar']);