diff --git a/examples/filter-points-webgl.html b/examples/filter-points-webgl.html index 068e4aa991..50a56a74e9 100644 --- a/examples/filter-points-webgl.html +++ b/examples/filter-points-webgl.html @@ -3,14 +3,14 @@ layout: example.html title: Filtering features with WebGL shortdesc: Using WebGL to filter large quantities of features docs: > - This example shows how to use `ol/renderer/webgl/PointsLayer` to dynamically filter a large amount + This example shows how to use `ol/layer/WebGLPoints` with a literal style to dynamically filter a large amount of point geometries. The above map is based on a dataset from the NASA containing 45k recorded meteorite landing sites. Each meteorite is marked by a circle on the map (the bigger the circle, the heavier the object). A pulse effect has been added, which is slightly offset by the year of the impact. - Adjusting the sliders causes the objects outside of the date range to be filtered out of the map. This is done using - a custom fragment shader on the layer renderer, and by using the `v_opacity` attribute of the rendered objects - to store the year of impact. + Adjusting the sliders causes the objects outside of the date range to be filtered out of the map. This is done + by mutating the variables in the `style` object provided to the WebGL layer. Also note that the last snippet + of code is necessary to make sure the map refreshes itself every frame. tags: "webgl, icon, sprite, filter, feature" experimental: true diff --git a/examples/filter-points-webgl.js b/examples/filter-points-webgl.js index e71b70cd97..96d63e02d4 100644 --- a/examples/filter-points-webgl.js +++ b/examples/filter-points-webgl.js @@ -3,162 +3,106 @@ import View from '../src/ol/View.js'; import TileLayer from '../src/ol/layer/Tile.js'; import Feature from '../src/ol/Feature.js'; import Point from '../src/ol/geom/Point.js'; -import VectorLayer from '../src/ol/layer/Vector.js'; import {Vector} from '../src/ol/source.js'; import {fromLonLat} from '../src/ol/proj.js'; -import WebGLPointsLayerRenderer from '../src/ol/renderer/webgl/PointsLayer.js'; -import {clamp} from '../src/ol/math.js'; import Stamen from '../src/ol/source/Stamen.js'; -import {formatColor} from '../src/ol/webgl/ShaderBuilder.js'; +import WebGLPointsLayer from '../src/ol/layer/WebGLPoints.js'; const vectorSource = new Vector({ attributions: 'NASA' }); -const oldColor = [180, 140, 140]; -const newColor = [255, 80, 80]; +const oldColor = 'rgba(242,56,22,0.61)'; +const newColor = '#ffe52c'; +const period = 12; // animation period in seconds +const animRatio = + ['pow', + ['/', + ['mod', + ['+', + ['time'], + ['stretch', ['get', 'year'], 1850, 2020, 0, period] + ], + period + ], + period + ], + 0.5 + ]; -const startTime = Date.now() * 0.001; +const style = { + variables: { + minYear: 1850, + maxYear: 2015 + }, + filter: ['between', ['get', 'year'], ['var', 'minYear'], ['var', 'maxYear']], + symbol: { + symbolType: 'circle', + size: ['*', + ['stretch', ['get', 'mass'], 0, 200000, 8, 26], + ['-', 1.5, ['*', animRatio, 0.5]] + ], + color: ['interpolate', + animRatio, + newColor, oldColor], + opacity: ['-', 1.0, ['*', animRatio, 0.75]] + } +}; -// hanle input values & events +// handle input values & events const minYearInput = document.getElementById('min-year'); const maxYearInput = document.getElementById('max-year'); + +function updateMinYear() { + style.variables.minYear = parseInt(minYearInput.value); + updateStatusText(); +} +function updateMaxYear() { + style.variables.maxYear = parseInt(maxYearInput.value); + updateStatusText(); +} function updateStatusText() { const div = document.getElementById('status'); div.querySelector('span.min-year').textContent = minYearInput.value; div.querySelector('span.max-year').textContent = maxYearInput.value; } -minYearInput.addEventListener('input', updateStatusText); -minYearInput.addEventListener('change', updateStatusText); -maxYearInput.addEventListener('input', updateStatusText); -maxYearInput.addEventListener('change', updateStatusText); + +minYearInput.addEventListener('input', updateMinYear); +minYearInput.addEventListener('change', updateMinYear); +maxYearInput.addEventListener('input', updateMaxYear); +maxYearInput.addEventListener('change', updateMaxYear); updateStatusText(); -class WebglPointsLayer extends VectorLayer { - createRenderer() { - return new WebGLPointsLayerRenderer(this, { - attributes: [ - { - name: 'size', - callback: function(feature) { - return 18 * clamp(feature.get('mass') / 200000, 0, 1) + 8; - } - }, - { - name: 'year', - callback: function(feature) { - return feature.get('year'); - } - } - ], - vertexShader: [ - 'precision mediump float;', +// load data +const client = new XMLHttpRequest(); +client.open('GET', 'data/csv/meteorite_landings.csv'); +client.onload = function() { + const csv = client.responseText; + const features = []; - 'uniform mat4 u_projectionMatrix;', - 'uniform mat4 u_offsetScaleMatrix;', - 'uniform mat4 u_offsetRotateMatrix;', - 'attribute vec2 a_position;', - 'attribute float a_index;', - 'attribute float a_size;', - 'attribute float a_year;', - 'varying vec2 v_texCoord;', - 'varying float v_year;', + let prevIndex = csv.indexOf('\n') + 1; // scan past the header line - 'void main(void) {', - ' mat4 offsetMatrix = u_offsetScaleMatrix;', - ' float offsetX = a_index == 0.0 || a_index == 3.0 ? -a_size / 2.0 : a_size / 2.0;', - ' float offsetY = a_index == 0.0 || a_index == 1.0 ? -a_size / 2.0 : a_size / 2.0;', - ' vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);', - ' gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;', - ' float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;', - ' float v = a_index == 0.0 || a_index == 1.0 ? 0.0 : 1.0;', - ' v_texCoord = vec2(u, v);', - ' v_year = a_year;', - '}' - ].join(' '), - fragmentShader: [ - 'precision mediump float;', + let curIndex; + while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) { + const line = csv.substr(prevIndex, curIndex - prevIndex).split(','); + prevIndex = curIndex + 1; - 'uniform float u_time;', - 'uniform float u_minYear;', - 'uniform float u_maxYear;', - 'varying vec2 v_texCoord;', - 'varying float v_year;', - - 'void main(void) {', - - // filter out pixels if the year is outside of the given range - ' if (v_year < u_minYear || v_year > u_maxYear) {', - ' discard;', - ' }', - - ' vec2 texCoord = v_texCoord * 2.0 - vec2(1.0, 1.0);', - ' float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y;', - ' float value = 2.0 * (1.0 - sqRadius);', - ' float alpha = smoothstep(0.0, 1.0, value);', - - // color is interpolated based on year - ' float ratio = clamp((v_year - 1800.0) / (2013.0 - 1800.0), 0.0, 1.1);', - ' vec3 color = mix(vec3(' + formatColor(oldColor) + '),', - ' vec3(' + formatColor(newColor) + '), ratio);', - - ' float period = 8.0;', - ' color.g *= 2.0 * (1.0 - sqrt(mod(u_time + v_year * 0.025, period) / period));', - - ' gl_FragColor = vec4(color, 1.0);', - ' gl_FragColor.a *= alpha;', - ' gl_FragColor.rgb *= gl_FragColor.a;', - '}' - ].join(' '), - uniforms: { - u_time: function() { - return Date.now() * 0.001 - startTime; - }, - u_minYear: function() { - return parseInt(minYearInput.value); - }, - u_maxYear: function() { - return parseInt(maxYearInput.value); - } - } - }); - } -} - - -function loadData() { - const client = new XMLHttpRequest(); - client.open('GET', 'data/csv/meteorite_landings.csv'); - client.onload = function() { - const csv = client.responseText; - const features = []; - - let prevIndex = csv.indexOf('\n') + 1; // scan past the header line - - let curIndex; - while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) { - const line = csv.substr(prevIndex, curIndex - prevIndex).split(','); - prevIndex = curIndex + 1; - - const coords = fromLonLat([parseFloat(line[4]), parseFloat(line[3])]); - if (isNaN(coords[0]) || isNaN(coords[1])) { - // guard against bad data - continue; - } - - features.push(new Feature({ - mass: parseFloat(line[1]) || 0, - year: parseInt(line[2]) || 0, - geometry: new Point(coords) - })); + const coords = fromLonLat([parseFloat(line[4]), parseFloat(line[3])]); + if (isNaN(coords[0]) || isNaN(coords[1])) { + // guard against bad data + continue; } - vectorSource.addFeatures(features); - }; - client.send(); -} + features.push(new Feature({ + mass: parseFloat(line[1]) || 0, + year: parseInt(line[2]) || 0, + geometry: new Point(coords) + })); + } -loadData(); + vectorSource.addFeatures(features); +}; +client.send(); const map = new Map({ layers: [ @@ -167,7 +111,8 @@ const map = new Map({ layer: 'toner' }) }), - new WebglPointsLayer({ + new WebGLPointsLayer({ + style: style, source: vectorSource }) ], diff --git a/examples/webgl-points-layer.js b/examples/webgl-points-layer.js index 18c7ba4c30..258cb995fa 100644 --- a/examples/webgl-points-layer.js +++ b/examples/webgl-points-layer.js @@ -27,10 +27,10 @@ const predefinedStyles = { symbolType: 'triangle', size: 18, color: [ - ['stretch', ['get', 'population'], 20000, 300000, 0.1, 1.0], - ['stretch', ['get', 'population'], 20000, 300000, 0.6, 0.3], - 0.6, - 1.0 + 'interpolate', + ['stretch', ['get', 'population'], 20000, 300000, 0, 1], + '#5aca5b', + '#ff6a19' ], rotateWithView: true } diff --git a/src/ol/color.js b/src/ol/color.js index b48f149e90..e523d94336 100644 --- a/src/ol/color.js +++ b/src/ol/color.js @@ -223,3 +223,14 @@ export function toString(color) { const a = color[3] === undefined ? 1 : color[3]; return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } + +/** + * @param {string} s String. + * @return {boolean} Whether the string is actually a valid color + */ +export function isStringColor(s) { + if (NAMED_COLOR_RE_.test(s)) { + s = fromNamed(s); + } + return HEX_COLOR_RE_.test(s) || s.indexOf('rgba(') === 0 || s.indexOf('rgb(') === 0; +} diff --git a/src/ol/layer/WebGLPoints.js b/src/ol/layer/WebGLPoints.js index f5b375f163..74fa63b039 100644 --- a/src/ol/layer/WebGLPoints.js +++ b/src/ol/layer/WebGLPoints.js @@ -3,7 +3,7 @@ */ import {assign} from '../obj.js'; import WebGLPointsLayerRenderer from '../renderer/webgl/PointsLayer.js'; -import {getSymbolFragmentShader, getSymbolVertexShader, parseSymbolStyle} from '../webgl/ShaderBuilder.js'; +import {parseLiteralStyle} from '../webgl/ShaderBuilder.js'; import Layer from './Layer.js'; @@ -74,7 +74,7 @@ class WebGLPointsLayer extends Layer { * @private * @type {import('../webgl/ShaderBuilder.js').StyleParseResult} */ - this.parseResult_ = parseSymbolStyle(options.style.symbol); + this.parseResult_ = parseLiteralStyle(options.style); } /** @@ -82,8 +82,8 @@ class WebGLPointsLayer extends Layer { */ createRenderer() { return new WebGLPointsLayerRenderer(this, { - vertexShader: getSymbolVertexShader(this.parseResult_.params), - fragmentShader: getSymbolFragmentShader(this.parseResult_.params), + vertexShader: this.parseResult_.builder.getSymbolVertexShader(), + fragmentShader: this.parseResult_.builder.getSymbolFragmentShader(), uniforms: this.parseResult_.uniforms, attributes: this.parseResult_.attributes }); diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index dc51eed214..71788a373c 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -132,6 +132,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { options.vertexShader ); + if (this.getShaderCompileErrors()) { + throw new Error(this.getShaderCompileErrors()); + } + /** * @type {boolean} * @private diff --git a/src/ol/style/LiteralStyle.js b/src/ol/style/LiteralStyle.js index f5811fb94d..c0ee1e085a 100644 --- a/src/ol/style/LiteralStyle.js +++ b/src/ol/style/LiteralStyle.js @@ -4,8 +4,58 @@ * @module ol/style/LiteralStyle */ +/** + * Base type used for literal style parameters; can be a number literal or the output of an operator, + * which in turns takes {@link ExpressionValue} arguments. + * + * The following operators can be used: + * + * * Reading operators: + * * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader) + * Note: those will be taken from the attributes provided to the renderer + * * `['var', 'varName']` fetches a value from the style variables, or 0 if undefined + * * `['time']` returns the time in seconds since the creation of the layer + * + * * Math operators: + * * `['*', value1, value1]` multiplies `value1` by `value2` + * * `['/', value1, value1]` divides `value1` by `value2` + * * `['+', value1, value1]` adds `value1` and `value2` + * * `['-', value1, value1]` subtracts `value2` from `value1` + * * `['clamp', value, low, high]` clamps `value` between `low` and `high` + * * `['stretch', value, low1, high1, low2, high2]` maps `value` from [`low1`, `high1`] range to + * [`low2`, `high2`] range, clamping values along the way + * * `['mod', value1, value1]` returns the result of `value1 % value2` (modulo) + * * `['pow', value1, value1]` returns the value of `value1` raised to the `value2` power + * + * * Color operators: + * * `['interpolate', ratio, color1, color2]` returns a color through interpolation between `color1` and + * `color2` with the given `ratio` + * + * * Logical operators: + * * `['<', value1, value2]` returns `1` if `value1` is strictly lower than value 2, or `0` otherwise. + * * `['<=', value1, value2]` returns `1` if `value1` is lower than or equals value 2, or `0` otherwise. + * * `['>', value1, value2]` returns `1` if `value1` is strictly greater than value 2, or `0` otherwise. + * * `['>=', value1, value2]` returns `1` if `value1` is greater than or equals value 2, or `0` otherwise. + * * `['==', value1, value2]` returns `1` if `value1` equals value 2, or `0` otherwise. + * * `['!', value1]` returns `0` if `value1` strictly greater than `0`, or `1` otherwise. + * * `['between', value1, value2, value3]` returns `1` if `value1` is contained between `value2` and `value3` + * (inclusively), or `0` otherwise. + * + * Values can either be literals or another operator, as they will be evaluated recursively. + * Literal values can be of the following types: + * * `number` + * * `string` + * * {@link module:ol/color~Color} + * + * @typedef {Array<*>|import("../color.js").Color|string|number} ExpressionValue + */ + /** * @typedef {Object} LiteralStyle + * @property {ExpressionValue} [filter] Filter expression. If it resolves to a number strictly greater than 0, the + * point will be displayed. If undefined, all points will show. + * @property {Object} [variables] Style variables; each variable must hold a number. + * Note: **this object is meant to be mutated**: changes to the values will immediately be visible on the rendered features * @property {LiteralSymbolStyle} [symbol] Symbol representation. */ @@ -22,12 +72,12 @@ export const SymbolType = { /** * @typedef {Object} LiteralSymbolStyle - * @property {number|Array.} size Size, mandatory. + * @property {ExpressionValue|Array} size Size, mandatory. * @property {SymbolType} symbolType Symbol type to use, either a regular shape or an image. * @property {string} [src] Path to the image to be used for the symbol. Only required with `symbolType: 'image'`. - * @property {import("../color.js").Color|string} [color='#FFFFFF'] Color used for the representation (either fill, line or symbol). - * @property {number} [opacity=1] Opacity. - * @property {Array.} [offset] Offset on X and Y axis for symbols. If not specified, the symbol will be centered. - * @property {Array.} [textureCoord] Texture coordinates. If not specified, the whole texture will be used (range for 0 to 1 on both axes). + * @property {import("../color.js").Color|Array|string} [color='#FFFFFF'] Color used for the representation (either fill, line or symbol). + * @property {ExpressionValue} [opacity=1] Opacity. + * @property {Array} [offset] Offset on X and Y axis for symbols. If not specified, the symbol will be centered. + * @property {Array} [textureCoord] Texture coordinates. If not specified, the whole texture will be used (range for 0 to 1 on both axes). * @property {boolean} [rotateWithView=false] Specify whether the symbol must rotate with the view or stay upwards. */ diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index 04c6eee152..288babfcfa 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -42,7 +42,8 @@ export const ShaderType = { export const DefaultUniform = { PROJECTION_MATRIX: 'u_projectionMatrix', OFFSET_SCALE_MATRIX: 'u_offsetScaleMatrix', - OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix' + OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix', + TIME: 'u_time' }; /** @@ -355,6 +356,12 @@ class WebGLHelper extends Disposable { * @private */ this.shaderCompileErrors_ = null; + + /** + * @type {number} + * @private + */ + this.startTime_ = Date.now(); } /** @@ -548,6 +555,8 @@ class WebGLHelper extends Disposable { this.setUniformMatrixValue(DefaultUniform.OFFSET_SCALE_MATRIX, fromTransform(this.tmpMat4_, offsetScaleMatrix)); this.setUniformMatrixValue(DefaultUniform.OFFSET_ROTATION_MATRIX, fromTransform(this.tmpMat4_, offsetRotateMatrix)); + + this.setUniformFloatValue(DefaultUniform.TIME, (Date.now() - this.startTime_) * 0.001); } /** diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index 20f5cd2750..6a90a01c4d 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -1,9 +1,9 @@ /** - * Utilities for generating shaders from literal style objects + * Classes and utilities for generating shaders from literal style objects * @module ol/webgl/ShaderBuilder */ -import {asArray} from '../color.js'; +import {asArray, isStringColor} from '../color.js'; /** * Will return the number as a float with a dot separator, which is required by GLSL. @@ -18,23 +18,302 @@ export function formatNumber(v) { /** * Will return the number array as a float with a dot separator, concatenated with ', '. * @param {Array} array Numerical values array. - * @returns {string} The array as string, e. g.: `1.0, 2.0, 3.0`. + * @returns {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`. */ export function formatArray(array) { - return array.map(formatNumber).join(', '); + if (array.length < 2 || array.length > 4) { + throw new Error('`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.'); + } + return `vec${array.length}(${array.map(formatNumber).join(', ')})`; } /** - * Will normalize and converts to string a color array compatible with GLSL. - * @param {Array} colorArray Color in [r, g, b, a] array form, with RGB components in the - * 0..255 range and the alpha component in the 0..1 range. Note that if the A component is - * missing, only 3 values will be output. - * @returns {string} The color components concatenated in `1.0, 1.0, 1.0, 1.0` form. + * Will normalize and converts to string a `vec4` color array compatible with GLSL. + * @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format, + * with RGB components in the 0..255 range and the alpha component in the 0..1 range. + * Note that the final array will always have 4 components. + * @returns {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form. */ -export function formatColor(colorArray) { - return colorArray.map(function(c, i) { - return i < 3 ? c / 255 : c; - }).map(formatNumber).join(', '); +export function formatColor(color) { + const array = asArray(color).slice(); + if (array.length < 4) { + array.push(1); + } + return formatArray( + array.map(function(c, i) { + return i < 3 ? c / 255 : c; + }) + ); +} + +/** + * Possible inferred types from a given value or expression + * @enum {number} + */ +export const ValueTypes = { + UNKNOWN: -1, + NUMBER: 0, + STRING: 1, + COLOR: 2, + COLOR_OR_STRING: 3 +}; + +function getValueType(value) { + if (typeof value === 'number') { + return ValueTypes.NUMBER; + } + if (typeof value === 'string') { + if (isStringColor(value)) { + return ValueTypes.COLOR_OR_STRING; + } + return ValueTypes.STRING; + } + if (!Array.isArray(value)) { + throw new Error(`Unrecognized value type: ${JSON.stringify(value)}`); + } + if (value.length === 3 || value.length === 4) { + const onlyNumbers = value.every(function(v) { + return typeof v === 'number'; + }); + if (onlyNumbers) { + return ValueTypes.COLOR; + } + } + if (typeof value[0] !== 'string') { + return ValueTypes.UNKNOWN; + } + switch (value[0]) { + case 'get': + case 'var': + case 'time': + case '*': + case '/': + case '+': + case '-': + case 'clamp': + case 'stretch': + case 'mod': + case 'pow': + case '>': + case '>=': + case '<': + case '<=': + case '==': + case '!': + case 'between': + return ValueTypes.NUMBER; + case 'interpolate': + return ValueTypes.COLOR; + default: + return ValueTypes.UNKNOWN; + } +} + +/** + * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. + * @returns {boolean} True if a numeric value, false otherwise + */ +export function isValueTypeNumber(value) { + return getValueType(value) === ValueTypes.NUMBER; +} + +/** + * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. + * @returns {boolean} True if a string value, false otherwise + */ +export function isValueTypeString(value) { + return getValueType(value) === ValueTypes.STRING || getValueType(value) === ValueTypes.COLOR_OR_STRING; +} + +/** + * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. + * @returns {boolean} True if a color value, false otherwise + */ +export function isValueTypeColor(value) { + return getValueType(value) === ValueTypes.COLOR || getValueType(value) === ValueTypes.COLOR_OR_STRING; +} + + +/** + * Check that the provided value or expression is valid, and that the types used are compatible. + * + * Will throw an exception if found to be invalid. + * + * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. + */ +export function check(value) { + // these will be used to validate types in the expressions + function checkNumber(value) { + if (!isValueTypeNumber(value)) { + throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`); + } + } + function checkColor(value) { + if (!isValueTypeColor(value)) { + throw new Error(`A color value was expected, got ${JSON.stringify(value)} instead`); + } + } + function checkString(value) { + if (!isValueTypeString(value)) { + throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`); + } + } + + // first check that the value is of a recognized kind + if (!isValueTypeColor(value) && !isValueTypeNumber(value) && !isValueTypeString(value)) { + throw new Error(`No type could be inferred from the following expression: ${JSON.stringify(value)}`); + } + + // check operator arguments + if (Array.isArray(value) && typeof value[0] === 'string') { + switch (value[0]) { + case 'get': + case 'var': + checkString(value[1]); + break; + case 'time': + break; + case '*': + case '/': + case '+': + case '-': + case 'mod': + case 'pow': + checkNumber(value[1]); + checkNumber(value[2]); + break; + case 'clamp': + checkNumber(value[1]); + checkNumber(value[2]); + checkNumber(value[3]); + break; + case 'stretch': + checkNumber(value[1]); + checkNumber(value[2]); + checkNumber(value[3]); + checkNumber(value[4]); + checkNumber(value[5]); + break; + case '>': + case '>=': + case '<': + case '<=': + case '==': + checkNumber(value[1]); + checkNumber(value[2]); + break; + case '!': + checkNumber(value[1]); + break; + case 'between': + checkNumber(value[1]); + checkNumber(value[2]); + checkNumber(value[3]); + break; + case 'interpolate': + checkNumber(value[1]); + checkColor(value[2]); + checkColor(value[3]); + break; + default: throw new Error(`Unrecognized operator in style expression: ${JSON.stringify(value)}`); + } + } +} + +/** + * Parses the provided expressions and produces a GLSL-compatible assignment string, such as: + * `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)' + * + * Also takes in two arrays where new attributes and variables will be pushed, so that the user of the `parse` function + * knows which attributes/variables are expected to be available at evaluation time. + * + * 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, typeHint) { + check(value); + + function p(value) { + return parse(value, attributes, attributePrefix, variables); + } + function pC(value) { + return parse(value, attributes, attributePrefix, variables, ValueTypes.COLOR); + } + + // operator + if (Array.isArray(value) && typeof value[0] === 'string') { + switch (value[0]) { + // reading operators + case 'get': + if (attributes.indexOf(value[1]) === -1) { + attributes.push(value[1]); + } + return attributePrefix + value[1]; + case 'var': + if (variables.indexOf(value[1]) === -1) { + variables.push(value[1]); + } + return `u_${value[1]}`; + case 'time': + return 'u_time'; + + // math operators + case '*': + case '/': + case '+': + case '-': + return `(${p(value[1])} ${value[0]} ${p(value[2])})`; + case 'clamp': return `clamp(${p(value[1])}, ${p(value[2])}, ${p(value[3])})`; + case 'stretch': + const low1 = p(value[2]); + const high1 = p(value[3]); + const low2 = p(value[4]); + const high2 = p(value[5]); + return `((clamp(${p(value[1])}, ${low1}, ${high1}) - ${low1}) * ((${high2} - ${low2}) / (${high1} - ${low1})) + ${low2})`; + case 'mod': return `mod(${p(value[1])}, ${p(value[2])})`; + case 'pow': return `pow(${p(value[1])}, ${p(value[2])})`; + + // color operators + case 'interpolate': + return `mix(${pC(value[2])}, ${pC(value[3])}, ${p(value[1])})`; + + // logical operators + case '>': + case '>=': + case '<': + case '<=': + case '==': + return `(${p(value[1])} ${value[0]} ${p(value[2])} ? 1.0 : 0.0)`; + case '!': + return `(${p(value[1])} > 0.0 ? 0.0 : 1.0)`; + case 'between': + return `(${p(value[1])} >= ${p(value[2])} && ${p(value[1])} <= ${p(value[3])} ? 1.0 : 0.0)`; + + default: throw new Error('Invalid style expression: ' + JSON.stringify(value)); + } + } else if (isValueTypeNumber(value)) { + return formatNumber(/** @type {number} */(value)); + } else if (isValueTypeString(value) && (typeHint === undefined || typeHint == ValueTypes.STRING)) { + return `"${value}"`; + } else { + return formatColor(/** @type {number[]|string} */(value)); + } } /** @@ -46,209 +325,357 @@ export function formatColor(colorArray) { */ /** - * @typedef {Object} ShaderParameters - * @property {Array} [uniforms] Uniforms; these will be declared in the header (should include the type). - * @property {Array} [attributes] Attributes; these will be declared in the header (should include the type). - * @property {Array} [varyings] Varyings with a name, a type and an expression. - * @property {string} sizeExpression This will be assigned to a `vec2 size` variable. - * @property {string} offsetExpression This will be assigned to a `vec2 offset` variable. - * @property {string} colorExpression This will be the value assigned to gl_FragColor - * @property {string} texCoordExpression This will be the value assigned to the `vec4 v_texCoord` varying. - * @property {boolean} [rotateWithView=false] Whether symbols should rotate with view + * @classdesc + * This class implements a classic builder pattern for generating many different types of shaders. + * Methods can be chained, e. g.: + * + * ```js + * const shader = new ShaderBuilder() + * .addVarying('v_width', 'float', 'a_width') + * .addUniform('u_time') + * .setColorExpression('...') + * .setSizeExpression('...') + * .outputSymbolFragmentShader(); + * ``` */ +export class ShaderBuilder { + constructor() { + /** + * Uniforms; these will be declared in the header (should include the type). + * @type {Array} + * @private + */ + this.uniforms = []; -/** - * Generates a symbol vertex shader from a set of parameters, - * intended to be used on point geometries. - * - * Three uniforms are hardcoded in all shaders: `u_projectionMatrix`, `u_offsetScaleMatrix` and - * `u_offsetRotateMatrix`. - * - * The following attributes are hardcoded and expected to be present in the vertex buffers: - * `vec2 a_position`, `float a_index` (being the index of the vertex in the quad, 0 to 3). - * - * The following varyings are hardcoded and gives the coordinate of the pixel both in the quad on the texture: - * `vec2 v_quadCoord`, `vec2 v_texCoord` - * - * @param {ShaderParameters} parameters Parameters for the shader. - * @returns {string} The full shader as a string. - */ -export function getSymbolVertexShader(parameters) { - const offsetMatrix = parameters.rotateWithView ? - 'u_offsetScaleMatrix * u_offsetRotateMatrix' : - 'u_offsetScaleMatrix'; + /** + * Attributes; these will be declared in the header (should include the type). + * @type {Array} + * @private + */ + this.attributes = []; - const uniforms = parameters.uniforms || []; - const attributes = parameters.attributes || []; - const varyings = parameters.varyings || []; + /** + * Varyings with a name, a type and an expression. + * @type {Array} + * @private + */ + this.varyings = []; - const body = `precision mediump float; + /** + * @type {string} + * @private + */ + this.sizeExpression = 'vec2(1.0)'; + + /** + * @type {string} + * @private + */ + this.offsetExpression = 'vec2(0.0)'; + + /** + * @type {string} + * @private + */ + this.colorExpression = 'vec4(1.0)'; + + /** + * @type {string} + * @private + */ + this.texCoordExpression = 'vec4(0.0, 0.0, 1.0, 1.0)'; + + /** + * @type {string} + * @private + */ + this.discardExpression = 'false'; + + /** + * @type {boolean} + * @private + */ + this.rotateWithView = false; + } + + /** + * Adds a uniform accessible in both fragment and vertex shaders. + * The given name should include a type, such as `sampler2D u_texture`. + * @param {string} name Uniform name + * @return {ShaderBuilder} the builder object + */ + addUniform(name) { + this.uniforms.push(name); + return this; + } + + /** + * Adds an attribute accessible in the vertex shader, read from the geometry buffer. + * The given name should include a type, such as `vec2 a_position`. + * @param {string} name Attribute name + * @return {ShaderBuilder} the builder object + */ + addAttribute(name) { + this.attributes.push(name); + return this; + } + + /** + * Adds a varying defined in the vertex shader and accessible from the fragment shader. + * The type and expression of the varying have to be specified separately. + * @param {string} name Varying name + * @param {'float'|'vec2'|'vec3'|'vec4'} type Type + * @param {string} expression Expression used to assign a value to the varying. + * @return {ShaderBuilder} the builder object + */ + addVarying(name, type, expression) { + this.varyings.push({ + name: name, + type: type, + expression: expression + }); + return this; + } + + /** + * Sets an expression to compute the size of the shape. + * This expression can use all the uniforms and attributes available + * in the vertex shader, and should evaluate to a `vec2` value. + * @param {string} expression Size expression + * @return {ShaderBuilder} the builder object + */ + setSizeExpression(expression) { + this.sizeExpression = expression; + return this; + } + + /** + * Sets an expression to compute the offset of the symbol from the point center. + * This expression can use all the uniforms and attributes available + * in the vertex shader, and should evaluate to a `vec2` value. + * Note: will only be used for point geometry shaders. + * @param {string} expression Offset expression + * @return {ShaderBuilder} the builder object + */ + setSymbolOffsetExpression(expression) { + this.offsetExpression = expression; + return this; + } + + /** + * Sets an expression to compute the color of the shape. + * This expression can use all the uniforms, varyings and attributes available + * in the fragment shader, and should evaluate to a `vec4` value. + * @param {string} expression Color expression + * @return {ShaderBuilder} the builder object + */ + setColorExpression(expression) { + this.colorExpression = expression; + return this; + } + + /** + * Sets an expression to compute the texture coordinates of the vertices. + * This expression can use all the uniforms and attributes available + * in the vertex shader, and should evaluate to a `vec4` value. + * @param {string} expression Texture coordinate expression + * @return {ShaderBuilder} the builder object + */ + setTextureCoordinateExpression(expression) { + this.texCoordExpression = expression; + return this; + } + + /** + * Sets an expression to determine whether a fragment (pixel) should be discarded, + * i.e. not drawn at all. + * This expression can use all the uniforms, varyings and attributes available + * in the fragment shader, and should evaluate to a `bool` value (it will be + * used in an `if` statement) + * @param {string} expression Fragment discard expression + * @return {ShaderBuilder} the builder object + */ + setFragmentDiscardExpression(expression) { + this.discardExpression = expression; + return this; + } + + /** + * Sets whether the symbols should rotate with the view or stay aligned with the map. + * Note: will only be used for point geometry shaders. + * @param {boolean} rotateWithView Rotate with view + * @return {ShaderBuilder} the builder object + */ + setSymbolRotateWithView(rotateWithView) { + this.rotateWithView = rotateWithView; + return this; + } + + /** + * @returns {string} Previously set size expression + */ + getSizeExpression() { + return this.sizeExpression; + } + + /** + * @returns {string} Previously set symbol offset expression + */ + getOffsetExpression() { + return this.offsetExpression; + } + + /** + * @returns {string} Previously set color expression + */ + getColorExpression() { + return this.colorExpression; + } + + /** + * @returns {string} Previously set texture coordinate expression + */ + getTextureCoordinateExpression() { + return this.texCoordExpression; + } + + /** + * @returns {string} Previously set fragment discard expression + */ + getFragmentDiscardExpression() { + return this.discardExpression; + } + + /** + * Generates a symbol vertex shader from the builder parameters, + * intended to be used on point geometries. + * + * Three uniforms are hardcoded in all shaders: `u_projectionMatrix`, `u_offsetScaleMatrix`, + * `u_offsetRotateMatrix`, `u_time`. + * + * The following attributes are hardcoded and expected to be present in the vertex buffers: + * `vec2 a_position`, `float a_index` (being the index of the vertex in the quad, 0 to 3). + * + * The following varyings are hardcoded and gives the coordinate of the pixel both in the quad and on the texture: + * `vec2 v_quadCoord`, `vec2 v_texCoord` + * + * @returns {string} The full shader as a string. + */ + getSymbolVertexShader() { + const offsetMatrix = this.rotateWithView ? + 'u_offsetScaleMatrix * u_offsetRotateMatrix' : + 'u_offsetScaleMatrix'; + + return `precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; -${uniforms.map(function(uniform) { +uniform float u_time; +${this.uniforms.map(function(uniform) { return 'uniform ' + uniform + ';'; }).join('\n')} attribute vec2 a_position; attribute float a_index; -${attributes.map(function(attribute) { +${this.attributes.map(function(attribute) { return 'attribute ' + attribute + ';'; }).join('\n')} varying vec2 v_texCoord; varying vec2 v_quadCoord; -${varyings.map(function(varying) { +${this.varyings.map(function(varying) { return 'varying ' + varying.type + ' ' + varying.name + ';'; }).join('\n')} void main(void) { mat4 offsetMatrix = ${offsetMatrix}; - vec2 size = ${parameters.sizeExpression}; - vec2 offset = ${parameters.offsetExpression}; + vec2 size = ${this.sizeExpression}; + vec2 offset = ${this.offsetExpression}; float offsetX = a_index == 0.0 || a_index == 3.0 ? offset.x - size.x / 2.0 : offset.x + size.x / 2.0; float offsetY = a_index == 0.0 || a_index == 1.0 ? offset.y - size.y / 2.0 : offset.y + size.y / 2.0; vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0); gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets; - vec4 texCoord = ${parameters.texCoordExpression}; + vec4 texCoord = ${this.texCoordExpression}; float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.q; float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.p; v_texCoord = vec2(u, v); u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0; v = a_index == 2.0 || a_index == 3.0 ? 0.0 : 1.0; v_quadCoord = vec2(u, v); -${varyings.map(function(varying) { +${this.varyings.map(function(varying) { return ' ' + varying.name + ' = ' + varying.expression + ';'; }).join('\n')} }`; + } - return body; -} - -/** - * Generates a symbol fragment shader intended to be used on point geometries. - * - * Expected the following varyings to be transmitted by the vertex shader: - * `vec2 v_texCoord` - * - * @param {ShaderParameters} parameters Parameters for the shader. - * @returns {string} The full shader as a string. - */ -export function getSymbolFragmentShader(parameters) { - const uniforms = parameters.uniforms || []; - const varyings = parameters.varyings || []; - - const body = `precision mediump float; -${uniforms.map(function(uniform) { + /** + * Generates a symbol fragment shader from the builder parameters, + * intended to be used on point geometries. + * + * Expects the following varyings to be transmitted by the vertex shader: + * `vec2 v_quadCoord`, `vec2 v_texCoord` + * + * @returns {string} The full shader as a string. + */ + getSymbolFragmentShader() { + return `precision mediump float; +uniform float u_time; +${this.uniforms.map(function(uniform) { return 'uniform ' + uniform + ';'; }).join('\n')} varying vec2 v_texCoord; varying vec2 v_quadCoord; -${varyings.map(function(varying) { +${this.varyings.map(function(varying) { return 'varying ' + varying.type + ' ' + varying.name + ';'; }).join('\n')} void main(void) { - gl_FragColor = ${parameters.colorExpression}; + if (${this.discardExpression}) { discard; } + gl_FragColor = ${this.colorExpression}; gl_FragColor.rgb *= gl_FragColor.a; }`; - - return body; -} - -/** - * Base type for values fed to operators; can be a number literal or the output of another operator - * @typedef {Array<*>|number} OperatorValue - */ - -/** - * Parses the provided expressions and produces a GLSL-compatible assignment string, such as: - * `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)' - * - * The following operators can be used: - * * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader) - * * `['*', value1, value1]` multiplies value1 by value2 - * * `['+', value1, value1]` adds value1 and value2 - * * `['clamp', value1, value2, value3]` clamps value1 between values2 and value3 - * * `['stretch', value1, value2, value3, value4, value5]` maps value1 from [value2, value3] range to - * [value4, value5] range, clamping values along the way - * - * Values can either be literals (numbers) or another operator, as they will be evaluated recursively. - * - * Also takes in an array where new attributes will be pushed, so that the user of the `parse` function - * knows which attributes are expected to be available at evaluation time. - * - * A prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in - * the final assignment string. - * - * @param {OperatorValue} value Either literal or an operator. - * @param {Array} attributes Array containing the attribute names prefixed with `a_`; it - * it passed along recursively - * @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`). - * @returns {string} Assignment string. - */ -export function parse(value, attributes, attributePrefix) { - const v = value; - function p(value) { - return parse(value, attributes, attributePrefix); - } - if (Array.isArray(v)) { - switch (v[0]) { - case 'get': - if (attributes.indexOf(v[1]) === -1) { - attributes.push(v[1]); - } - return attributePrefix + v[1]; - case '*': return `(${p(v[1])} * ${p(v[2])})`; - case '+': return `(${p(v[1])} + ${p(v[2])})`; - 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])})`; - default: throw new Error('Unrecognized literal style expression: ' + JSON.stringify(value)); - } - } else if (typeof value === 'number') { - return formatNumber(value); - } else { - throw new Error('Invalid value type in expression: ' + JSON.stringify(value)); } } /** * @typedef {Object} StyleParseResult - * @property {ShaderParameters} params Symbol shader params. + * @property {ShaderBuilder} builder Shader builder pre-configured according to a given style * @property {Object.} uniforms Uniform definitions. * @property {Array} attributes Attribute descriptions. */ /** - * Parses a {@link import("../style/LiteralStyle").LiteralSymbolStyle} object and outputs shader parameters to be - * then fed to {@link getSymbolVertexShader} and {@link getSymbolFragmentShader}. + * Parses a {@link import("../style/LiteralStyle").LiteralStyle} object and returns a {@link ShaderBuilder} + * object that has been configured according to the given style, as well as `attributes` and `uniforms` + * arrays to be fed to the `WebGLPointsRenderer` class. * * Also returns `uniforms` and `attributes` properties as expected by the * {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}. * - * @param {import("../style/LiteralStyle").LiteralSymbolStyle} style Symbol style. + * @param {import("../style/LiteralStyle").LiteralStyle} style Literal style. * @returns {StyleParseResult} Result containing shader params, attributes and uniforms. */ -export function parseSymbolStyle(style) { - const size = Array.isArray(style.size) && typeof style.size[0] == 'number' ? - style.size : [style.size, style.size]; - const color = (typeof style.color === 'string' ? - asArray(style.color).map(function(c, i) { - return i < 3 ? c / 255 : c; - }) : - style.color || [255, 255, 255, 1]); - const texCoord = style.textureCoord || [0, 0, 1, 1]; - const offset = style.offset || [0, 0]; - const opacity = style.opacity !== undefined ? style.opacity : 1; +export function parseLiteralStyle(style) { + const symbStyle = style.symbol; + const size = Array.isArray(symbStyle.size) && typeof symbStyle.size[0] == 'number' ? + symbStyle.size : [symbStyle.size, symbStyle.size]; + const color = symbStyle.color || 'white'; + const texCoord = symbStyle.textureCoord || [0, 0, 1, 1]; + const offset = symbStyle.offset || [0, 0]; + const opacity = symbStyle.opacity !== undefined ? symbStyle.opacity : 1; - let attributes = []; - const varyings = []; - function pA(value) { - return parse(value, attributes, 'a_'); + const variables = []; + const vertAttributes = []; + // parse function for vertex shader + function pVert(value) { + return parse(value, vertAttributes, 'a_', variables); } - function pV(value) { - return parse(value, varyings, 'v_'); + + const fragAttributes = []; + // parse function for fragment shader + function pFrag(value, type) { + return parse(value, fragAttributes, 'v_', variables, type); } let opacityFilter = '1.0'; - const visibleSize = pV(size[0]); - switch (style.symbolType) { + const visibleSize = pFrag(size[0]); + switch (symbStyle.symbolType) { case 'square': break; case 'image': break; // taken from https://thebookofshaders.com/07/ @@ -261,49 +688,62 @@ export function parseSymbolStyle(style) { opacityFilter = `(1.0-smoothstep(.5-3./${visibleSize},.5,cos(floor(.5+${a}/2.094395102)*2.094395102-${a})*length(${st})))`; break; - default: throw new Error('Unexpected symbol type: ' + style.symbolType); + default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType); } - /** @type {import('../webgl/ShaderBuilder.js').ShaderParameters} */ - const params = { - uniforms: [], - colorExpression: `vec4(${pV(color[0])}, ${pV(color[1])}, ${pV(color[2])}, ${pV(color[3])})` + - ` * vec4(1.0, 1.0, 1.0, ${pV(opacity)} * ${opacityFilter})`, - sizeExpression: `vec2(${pA(size[0])}, ${pA(size[1])})`, - offsetExpression: `vec2(${pA(offset[0])}, ${pA(offset[1])})`, - texCoordExpression: `vec4(${pA(texCoord[0])}, ${pA(texCoord[1])}, ${pA(texCoord[2])}, ${pA(texCoord[3])})`, - rotateWithView: !!style.rotateWithView - }; + const parsedColor = pFrag(color, ValueTypes.COLOR); - attributes = attributes.concat(varyings).filter(function(attrName, index, arr) { - return arr.indexOf(attrName) === index; - }); - params.attributes = attributes.map(function(attributeName) { - return `float a_${attributeName}`; - }); - params.varyings = varyings.map(function(attributeName) { - return { - name: `v_${attributeName}`, - type: 'float', - expression: `a_${attributeName}` - }; - }); + const builder = new ShaderBuilder() + .setSizeExpression(`vec2(${pVert(size[0])}, ${pVert(size[1])})`) + .setSymbolOffsetExpression(`vec2(${pVert(offset[0])}, ${pVert(offset[1])})`) + .setTextureCoordinateExpression( + `vec4(${pVert(texCoord[0])}, ${pVert(texCoord[1])}, ${pVert(texCoord[2])}, ${pVert(texCoord[3])})`) + .setSymbolRotateWithView(!!symbStyle.rotateWithView) + .setColorExpression( + `vec4(${parsedColor}.rgb, ${parsedColor}.a * ${pFrag(opacity)} * ${opacityFilter})`); + + if (style.filter) { + builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`); + } /** @type {Object.} */ const uniforms = {}; - if (style.symbolType === 'image' && style.src) { + // define one uniform per variable + variables.forEach(function(varName) { + builder.addUniform(`float u_${varName}`); + uniforms[`u_${varName}`] = function() { + return style.variables && style.variables[varName] !== undefined ? + style.variables[varName] : 0; + }; + }); + + if (symbStyle.symbolType === 'image' && symbStyle.src) { const texture = new Image(); - texture.src = style.src; - params.uniforms.push('sampler2D u_texture'); - params.colorExpression = params.colorExpression + - ' * texture2D(u_texture, v_texCoord)'; + texture.src = symbStyle.src; + builder.addUniform('sampler2D u_texture') + .setColorExpression(builder.getColorExpression() + + ' * texture2D(u_texture, v_texCoord)'); uniforms['u_texture'] = texture; } + // for each feature attribute used in the fragment shader, define a varying that will be used to pass data + // from the vertex to the fragment shader, as well as an attribute in the vertex shader (if not already present) + fragAttributes.forEach(function(attrName) { + if (vertAttributes.indexOf(attrName) === -1) { + vertAttributes.push(attrName); + } + builder.addVarying(`v_${attrName}`, 'float', `a_${attrName}`); + }); + + // for each feature attribute used in the vertex shader, define an attribute in the vertex shader. + vertAttributes.forEach(function(attrName) { + builder.addAttribute(`float a_${attrName}`); + }); + return { - params: params, - attributes: attributes.map(function(attributeName) { + builder: builder, + attributes: vertAttributes.map(function(attributeName) { return { name: attributeName, callback: function(feature) { diff --git a/test/spec/ol/color.test.js b/test/spec/ol/color.test.js index b230e289ad..a4099e747d 100644 --- a/test/spec/ol/color.test.js +++ b/test/spec/ol/color.test.js @@ -2,6 +2,7 @@ import { asArray, asString, fromString, + isStringColor, normalize, toString } from '../../../src/ol/color.js'; @@ -159,4 +160,18 @@ describe('ol.color', function() { }); }); + + describe('isValid()', function() { + + it('correctly detects valid colors', function() { + expect(isStringColor('rgba(1,3,4,0.4)')).to.be(true); + expect(isStringColor('rgb(1,3,4)')).to.be(true); + expect(isStringColor('lightgreen')).to.be(true); + expect(isStringColor('yellow')).to.be(true); + expect(isStringColor('GREEN')).to.be(true); + expect(isStringColor('notacolor')).to.be(false); + expect(isStringColor('red_')).to.be(false); + }); + + }); }); diff --git a/test/spec/ol/webgl/helper.test.js b/test/spec/ol/webgl/helper.test.js index 9e3bd65583..11f8c7eb20 100644 --- a/test/spec/ol/webgl/helper.test.js +++ b/test/spec/ol/webgl/helper.test.js @@ -1,4 +1,4 @@ -import WebGLHelper from '../../../../src/ol/webgl/Helper.js'; +import WebGLHelper, {DefaultUniform} from '../../../../src/ol/webgl/Helper.js'; import { create as createTransform, rotate as rotateTransform, @@ -11,9 +11,9 @@ import {FLOAT} from '../../../../src/ol/webgl.js'; const VERTEX_SHADER = ` precision mediump float; - uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; + uniform float u_time; attribute float a_test; uniform float u_test; @@ -25,9 +25,9 @@ const VERTEX_SHADER = ` const INVALID_VERTEX_SHADER = ` precision mediump float; - uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; + uniform float u_time; bla uniform float u_test; @@ -123,6 +123,12 @@ describe('ol.webgl.WebGLHelper', function() { expect(h.getCanvas().height).to.eql(160); }); + it('has processed default uniforms', function() { + expect(h.uniformLocations_[DefaultUniform.OFFSET_ROTATION_MATRIX]).not.to.eql(undefined); + expect(h.uniformLocations_[DefaultUniform.OFFSET_SCALE_MATRIX]).not.to.eql(undefined); + expect(h.uniformLocations_[DefaultUniform.TIME]).not.to.eql(undefined); + }); + it('has processed uniforms', function() { expect(h.uniforms_.length).to.eql(4); expect(h.uniforms_[0].name).to.eql('u_test1'); diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index e29f426b32..bc94e59bdc 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -1,8 +1,15 @@ import { - getSymbolVertexShader, + check, + formatArray, + formatColor, formatNumber, - getSymbolFragmentShader, - formatColor, formatArray, parse, parseSymbolStyle + isValueTypeColor, + isValueTypeNumber, + isValueTypeString, + parse, + parseLiteralStyle, + ShaderBuilder, + ValueTypes } from '../../../../src/ol/webgl/ShaderBuilder.js'; describe('ol.webgl.ShaderBuilder', function() { @@ -19,39 +26,93 @@ describe('ol.webgl.ShaderBuilder', function() { describe('formatArray', function() { it('outputs numbers with dot separators', function() { - expect(formatArray([1, 0, 3.45, 0.8888])).to.eql('1.0, 0.0, 3.45, 0.8888'); + expect(formatArray([1, 0, 3.45, 0.8888])).to.eql('vec4(1.0, 0.0, 3.45, 0.8888)'); + expect(formatArray([3, 4])).to.eql('vec2(3.0, 4.0)'); + }); + it('throws on invalid lengths', function() { + let thrown = false; + try { + formatArray([3]); + } catch (e) { + thrown = true; + } + try { + formatArray([3, 2, 1, 0, -1]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); }); }); describe('formatColor', function() { it('normalizes color and outputs numbers with dot separators', function() { - expect(formatColor([100, 0, 255, 1])).to.eql('0.39215686274509803, 0.0, 1.0, 1.0'); + expect(formatColor([100, 0, 255])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)'); + expect(formatColor([100, 0, 255, 1])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)'); + }); + it('handles colors in string format', function() { + expect(formatColor('red')).to.eql('vec4(1.0, 0.0, 0.0, 1.0)'); + expect(formatColor('#00ff99')).to.eql('vec4(0.0, 1.0, 0.6, 1.0)'); + expect(formatColor('rgb(100, 0, 255)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)'); + expect(formatColor('rgba(100, 0, 255, 0.3)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 0.3)'); + }); + }); + + describe('value type checking', function() { + it('correctly recognizes a number value', function() { + expect(isValueTypeNumber(1234)).to.eql(true); + expect(isValueTypeNumber(['time'])).to.eql(true); + expect(isValueTypeNumber(['clamp', ['get', 'attr'], -1, 1])).to.eql(true); + expect(isValueTypeNumber(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false); + expect(isValueTypeNumber('yellow')).to.eql(false); + expect(isValueTypeNumber('#113366')).to.eql(false); + expect(isValueTypeNumber('rgba(252,171,48,0.62)')).to.eql(false); + }); + it('correctly recognizes a color value', function() { + expect(isValueTypeColor(1234)).to.eql(false); + expect(isValueTypeColor(['time'])).to.eql(false); + expect(isValueTypeColor(['clamp', ['get', 'attr'], -1, 1])).to.eql(false); + expect(isValueTypeColor(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(true); + expect(isValueTypeColor('yellow')).to.eql(true); + expect(isValueTypeColor('#113366')).to.eql(true); + expect(isValueTypeColor('rgba(252,171,48,0.62)')).to.eql(true); + expect(isValueTypeColor('abcd')).to.eql(false); + }); + it('correctly recognizes a string value', function() { + expect(isValueTypeString(1234)).to.eql(false); + expect(isValueTypeString(['time'])).to.eql(false); + expect(isValueTypeString(['clamp', ['get', 'attr'], -1, 1])).to.eql(false); + expect(isValueTypeString(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false); + expect(isValueTypeString('yellow')).to.eql(true); + expect(isValueTypeString('#113366')).to.eql(true); + expect(isValueTypeString('rgba(252,171,48,0.62)')).to.eql(true); + expect(isValueTypeString('abcd')).to.eql(true); + }); + it('throws on an unsupported type', function(done) { + try { + isValueTypeColor(true); + } catch (e) { + done(); + } + done(true); }); }); describe('getSymbolVertexShader', function() { it('generates a symbol vertex shader (with varying)', function() { - const parameters = { - varyings: [{ - name: 'v_opacity', - type: 'float', - expression: formatNumber(0.4) - }, { - name: 'v_test', - type: 'vec3', - expression: 'vec3(' + formatArray([1, 2, 3]) + ')' - }], - sizeExpression: 'vec2(' + formatNumber(6) + ')', - offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', - colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', - texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')', - rotateWithView: false - }; + const builder = new ShaderBuilder(); + builder.addVarying('v_opacity', 'float', formatNumber(0.4)); + builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3])); + builder.setSizeExpression(`vec2(${formatNumber(6)})`); + builder.setSymbolOffsetExpression(formatArray([5, -7])); + builder.setColorExpression(formatColor([80, 0, 255, 1])); + builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); - expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; + expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; +uniform float u_time; attribute vec2 a_position; attribute float a_index; @@ -80,19 +141,19 @@ void main(void) { }`); }); it('generates a symbol vertex shader (with uniforms and attributes)', function() { - const parameters = { - uniforms: ['float u_myUniform'], - attributes: ['vec2 a_myAttr'], - sizeExpression: 'vec2(' + formatNumber(6) + ')', - offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', - colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', - texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')' - }; + const builder = new ShaderBuilder(); + builder.addUniform('float u_myUniform'); + builder.addAttribute('vec2 a_myAttr'); + builder.setSizeExpression(`vec2(${formatNumber(6)})`); + builder.setSymbolOffsetExpression(formatArray([5, -7])); + builder.setColorExpression(formatColor([80, 0, 255, 1])); + builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); - expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; + expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; +uniform float u_time; uniform float u_myUniform; attribute vec2 a_position; attribute float a_index; @@ -119,18 +180,18 @@ void main(void) { }`); }); it('generates a symbol vertex shader (with rotateWithView)', function() { - const parameters = { - sizeExpression: 'vec2(' + formatNumber(6) + ')', - offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', - colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', - texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')', - rotateWithView: true - }; + const builder = new ShaderBuilder(); + builder.setSizeExpression(`vec2(${formatNumber(6)})`); + builder.setSymbolOffsetExpression(formatArray([5, -7])); + builder.setColorExpression(formatColor([80, 0, 255, 1])); + builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); + builder.setSymbolRotateWithView(true); - expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; + expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; +uniform float u_time; attribute vec2 a_position; attribute float a_index; @@ -160,161 +221,365 @@ void main(void) { describe('getSymbolFragmentShader', function() { it('generates a symbol fragment shader (with varying)', function() { - const parameters = { - varyings: [{ - name: 'v_opacity', - type: 'float', - expression: formatNumber(0.4) - }, { - name: 'v_test', - type: 'vec3', - expression: 'vec3(' + formatArray([1, 2, 3]) + ')' - }], - sizeExpression: 'vec2(' + formatNumber(6) + ')', - offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', - colorExpression: 'vec4(' + formatColor([80, 0, 255]) + ', v_opacity)', - texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')', - rotateWithView: false - }; + const builder = new ShaderBuilder(); + builder.addVarying('v_opacity', 'float', formatNumber(0.4)); + builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3])); + builder.setSizeExpression(`vec2(${formatNumber(6)})`); + builder.setSymbolOffsetExpression(formatArray([5, -7])); + builder.setColorExpression(formatColor([80, 0, 255])); + builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); - expect(getSymbolFragmentShader(parameters)).to.eql(`precision mediump float; + expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float; +uniform float u_time; varying vec2 v_texCoord; varying vec2 v_quadCoord; varying float v_opacity; varying vec3 v_test; void main(void) { - gl_FragColor = vec4(0.3137254901960784, 0.0, 1.0, v_opacity); + if (false) { discard; } + gl_FragColor = vec4(0.3137254901960784, 0.0, 1.0, 1.0); gl_FragColor.rgb *= gl_FragColor.a; }`); }); it('generates a symbol fragment shader (with uniforms)', function() { - const parameters = { - uniforms: ['float u_myUniform', 'vec2 u_myUniform2'], - sizeExpression: 'vec2(' + formatNumber(6) + ')', - offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', - colorExpression: 'vec4(' + formatColor([255, 255, 255, 1]) + ')', - texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')' - }; + const builder = new ShaderBuilder(); + builder.addUniform('float u_myUniform'); + builder.addUniform('vec2 u_myUniform2'); + builder.setSizeExpression(`vec2(${formatNumber(6)})`); + builder.setSymbolOffsetExpression(formatArray([5, -7])); + builder.setColorExpression(formatColor([255, 255, 255, 1])); + builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1])); + builder.setFragmentDiscardExpression('u_myUniform > 0.5'); - expect(getSymbolFragmentShader(parameters)).to.eql(`precision mediump float; + expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float; +uniform float u_time; uniform float u_myUniform; uniform vec2 u_myUniform2; varying vec2 v_texCoord; varying vec2 v_quadCoord; void main(void) { + if (u_myUniform > 0.5) { discard; } gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); gl_FragColor.rgb *= gl_FragColor.a; }`); }); }); + describe('check', function() { + + 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']); + check(['+', ['*', ['get', 'size'], 0.001], 12]); + check(['/', ['-', ['get', 'size'], 0.001], 12]); + check(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20]); + check(['stretch', ['get', 'size'], 10, 100, 4, 8]); + check(['mod', ['pow', ['get', 'size'], 0.5], 12]); + check(['>', 10, ['get', 'attr4']]); + check(['>=', 10, ['get', 'attr4']]); + check(['<', 10, ['get', 'attr4']]); + check(['<=', 10, ['get', 'attr4']]); + 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(); + }); + + it('throws on unsupported types for operators', function() { + let thrown = false; + try { + check(['var', 1234]); + } catch (e) { + thrown = true; + } + try { + check(['<', 0, 'aa']); + } catch (e) { + thrown = true; + } + try { + check(['+', true, ['get', 'attr']]); + } catch (e) { + thrown = true; + } + try { + check(['interpolate', ['get', 'attr4'], 1, '#3344FF']); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + it('throws with the wrong number of arguments', function() { + let thrown = false; + try { + check(['var', 1234, 456]); + } catch (e) { + thrown = true; + } + try { + check(['<', 4]); + } catch (e) { + thrown = true; + } + try { + check(['+']); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + it('throws on invalid expressions', function() { + let thrown = false; + try { + check(true); + } catch (e) { + thrown = true; + } + try { + check([123, 456]); + } catch (e) { + thrown = true; + } + try { + check(null); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + }); + describe('parse', function() { - let attributes, prefix, parseFn; + let attributes, prefix, variables, parseFn; beforeEach(function() { attributes = []; + variables = []; prefix = 'a_'; - parseFn = function(value) { - return parse(value, attributes, prefix); + 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'); expect(parseFn(['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)'); + expect(parseFn(['/', ['-', ['get', 'size'], 20], 100])).to.eql('((a_size - 20.0) / 100.0)'); expect(parseFn(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)'); - expect(parseFn(['stretch', ['get', 'size'], 10, 100, 4, 8])).to.eql('(clamp(a_size, 10.0, 100.0) * ((8.0 - 4.0) / (100.0 - 10.0)) + 4.0)'); - expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3']); + expect(parseFn(['stretch', ['get', 'size'], 10, 100, 4, 8])).to.eql('((clamp(a_size, 10.0, 100.0) - 10.0) * ((8.0 - 4.0) / (100.0 - 10.0)) + 4.0)'); + expect(parseFn(['pow', ['mod', ['time'], 10], 2])).to.eql('pow(mod(u_time, 10.0), 2.0)'); + expect(parseFn(['>', 10, ['get', 'attr4']])).to.eql('(10.0 > a_attr4 ? 1.0 : 0.0)'); + expect(parseFn(['>=', 10, ['get', 'attr4']])).to.eql('(10.0 >= a_attr4 ? 1.0 : 0.0)'); + expect(parseFn(['<', 10, ['get', 'attr4']])).to.eql('(10.0 < a_attr4 ? 1.0 : 0.0)'); + expect(parseFn(['<=', 10, ['get', 'attr4']])).to.eql('(10.0 <= a_attr4 ? 1.0 : 0.0)'); + 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']); parseFn(['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]); + parseFn(['*', ['get', 'attr2'], ['var', 'myVar']]); expect(attributes).to.eql(['myAttr', 'attr2']); + expect(variables).to.eql(['myVar']); }); }); describe('parseSymbolStyle', function() { it('parses a style without expressions', function() { - const result = parseSymbolStyle({ - symbolType: 'square', - size: [4, 8], - color: '#336699', - rotateWithView: true - }); - expect(result.params).to.eql({ - uniforms: [], - attributes: [], - varyings: [], - colorExpression: 'vec4(0.2, 0.4, 0.6, 1.0) * vec4(1.0, 1.0, 1.0, 1.0 * 1.0)', - sizeExpression: 'vec2(4.0, 8.0)', - offsetExpression: 'vec2(0.0, 0.0)', - texCoordExpression: 'vec4(0.0, 0.0, 1.0, 1.0)', - rotateWithView: true + const result = parseLiteralStyle({ + symbol: { + symbolType: 'square', + size: [4, 8], + color: '#ff0000', + rotateWithView: true + } }); + + expect(result.builder.uniforms).to.eql([]); + expect(result.builder.attributes).to.eql([]); + expect(result.builder.varyings).to.eql([]); + expect(result.builder.colorExpression).to.eql( + 'vec4(vec4(1.0, 0.0, 0.0, 1.0).rgb, vec4(1.0, 0.0, 0.0, 1.0).a * 1.0 * 1.0)'); + expect(result.builder.sizeExpression).to.eql('vec2(4.0, 8.0)'); + expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)'); + expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)'); + expect(result.builder.rotateWithView).to.eql(true); expect(result.attributes).to.eql([]); expect(result.uniforms).to.eql({}); }); it('parses a style with expressions', function() { - const result = parseSymbolStyle({ - symbolType: 'square', - size: ['get', 'attr1'], - color: [ - 1.0, 0.0, 0.5, ['get', 'attr2'] - ], - textureCoord: [0.5, 0.5, 0.5, 1], - offset: [3, ['get', 'attr3']] + const result = parseLiteralStyle({ + symbol: { + symbolType: 'square', + size: ['get', 'attr1'], + color: [255, 127.5, 63.75, 0.25], + textureCoord: [0.5, 0.5, 0.5, 1], + offset: [3, ['get', 'attr3']] + } }); - expect(result.params).to.eql({ - uniforms: [], - attributes: ['float a_attr1', 'float a_attr3', 'float a_attr2'], - varyings: [{ - name: 'v_attr1', - type: 'float', - expression: 'a_attr1' - }, { - name: 'v_attr2', - type: 'float', - expression: 'a_attr2' - }], - colorExpression: 'vec4(1.0, 0.0, 0.5, v_attr2) * vec4(1.0, 1.0, 1.0, 1.0 * 1.0)', - sizeExpression: 'vec2(a_attr1, a_attr1)', - offsetExpression: 'vec2(3.0, a_attr3)', - texCoordExpression: 'vec4(0.5, 0.5, 0.5, 1.0)', - rotateWithView: false - }); - expect(result.attributes.length).to.eql(3); + + expect(result.builder.uniforms).to.eql([]); + expect(result.builder.attributes).to.eql(['float a_attr1', 'float a_attr3']); + expect(result.builder.varyings).to.eql([{ + name: 'v_attr1', + type: 'float', + expression: 'a_attr1' + }]); + expect(result.builder.colorExpression).to.eql( + 'vec4(vec4(1.0, 0.5, 0.25, 0.25).rgb, vec4(1.0, 0.5, 0.25, 0.25).a * 1.0 * 1.0)' + ); + expect(result.builder.sizeExpression).to.eql('vec2(a_attr1, a_attr1)'); + expect(result.builder.offsetExpression).to.eql('vec2(3.0, a_attr3)'); + expect(result.builder.texCoordExpression).to.eql('vec4(0.5, 0.5, 0.5, 1.0)'); + expect(result.builder.rotateWithView).to.eql(false); + expect(result.attributes.length).to.eql(2); expect(result.attributes[0].name).to.eql('attr1'); expect(result.attributes[1].name).to.eql('attr3'); - expect(result.attributes[2].name).to.eql('attr2'); expect(result.uniforms).to.eql({}); }); it('parses a style with a uniform (texture)', function() { - const result = parseSymbolStyle({ - symbolType: 'image', - src: '../data/image.png', - size: 6, - color: '#336699', - opacity: 0.5 - }); - expect(result.params).to.eql({ - uniforms: ['sampler2D u_texture'], - attributes: [], - varyings: [], - colorExpression: 'vec4(0.2, 0.4, 0.6, 1.0) * vec4(1.0, 1.0, 1.0, 0.5 * 1.0) * texture2D(u_texture, v_texCoord)', - sizeExpression: 'vec2(6.0, 6.0)', - offsetExpression: 'vec2(0.0, 0.0)', - texCoordExpression: 'vec4(0.0, 0.0, 1.0, 1.0)', - rotateWithView: false + const result = parseLiteralStyle({ + symbol: { + symbolType: 'image', + src: '../data/image.png', + size: 6, + color: '#336699', + opacity: 0.5 + } }); + + expect(result.builder.uniforms).to.eql(['sampler2D u_texture']); + expect(result.builder.attributes).to.eql([]); + expect(result.builder.varyings).to.eql([]); + expect(result.builder.colorExpression).to.eql( + 'vec4(vec4(0.2, 0.4, 0.6, 1.0).rgb, vec4(0.2, 0.4, 0.6, 1.0).a * 0.5 * 1.0) * texture2D(u_texture, v_texCoord)' + ); + expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)'); + expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)'); + expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)'); + expect(result.builder.rotateWithView).to.eql(false); expect(result.attributes).to.eql([]); expect(result.uniforms).to.have.property('u_texture'); }); + + it('parses a style with variables', function() { + const result = parseLiteralStyle({ + variables: { + lower: 100, + higher: 400 + }, + symbol: { + symbolType: 'square', + size: ['stretch', ['get', 'population'], ['var', 'lower'], ['var', 'higher'], 4, 8], + color: '#336699', + opacity: 0.5 + } + }); + + expect(result.builder.uniforms).to.eql(['float u_lower', 'float u_higher']); + expect(result.builder.attributes).to.eql(['float a_population']); + expect(result.builder.varyings).to.eql([{ + name: 'v_population', + type: 'float', + expression: 'a_population' + }]); + expect(result.builder.colorExpression).to.eql( + 'vec4(vec4(0.2, 0.4, 0.6, 1.0).rgb, vec4(0.2, 0.4, 0.6, 1.0).a * 0.5 * 1.0)' + ); + expect(result.builder.sizeExpression).to.eql( + 'vec2(((clamp(a_population, u_lower, u_higher) - u_lower) * ((8.0 - 4.0) / (u_higher - u_lower)) + 4.0), ((clamp(a_population, u_lower, u_higher) - u_lower) * ((8.0 - 4.0) / (u_higher - u_lower)) + 4.0))' + ); + expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)'); + expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)'); + expect(result.builder.rotateWithView).to.eql(false); + expect(result.attributes.length).to.eql(1); + expect(result.attributes[0].name).to.eql('population'); + expect(result.uniforms).to.have.property('u_lower'); + expect(result.uniforms).to.have.property('u_higher'); + }); + + it('parses a style with a filter', function() { + const result = parseLiteralStyle({ + filter: ['between', ['get', 'attr0'], 0, 10], + symbol: { + symbolType: 'square', + size: 6, + color: '#336699' + } + }); + + expect(result.builder.attributes).to.eql(['float a_attr0']); + expect(result.builder.varyings).to.eql([{ + name: 'v_attr0', + type: 'float', + expression: 'a_attr0' + }]); + expect(result.builder.colorExpression).to.eql( + 'vec4(vec4(0.2, 0.4, 0.6, 1.0).rgb, vec4(0.2, 0.4, 0.6, 1.0).a * 1.0 * 1.0)' + ); + expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)'); + expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)'); + expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)'); + expect(result.builder.discardExpression).to.eql('(v_attr0 >= 0.0 && v_attr0 <= 10.0 ? 1.0 : 0.0) <= 0.0'); + expect(result.builder.rotateWithView).to.eql(false); + expect(result.attributes.length).to.eql(1); + expect(result.attributes[0].name).to.eql('attr0'); + }); + + it('parses a style with a color interpolation', function() { + const result = parseLiteralStyle({ + symbol: { + symbolType: 'square', + size: 6, + color: ['interpolate', ['var', 'ratio'], [255, 255, 0], 'red'] + } + }); + + expect(result.builder.attributes).to.eql([]); + expect(result.builder.varyings).to.eql([]); + expect(result.builder.colorExpression).to.eql( + 'vec4(mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), u_ratio).rgb, mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), u_ratio).a * 1.0 * 1.0)' + ); + expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)'); + expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)'); + expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)'); + expect(result.builder.rotateWithView).to.eql(false); + expect(result.attributes).to.eql([]); + expect(result.uniforms).to.have.property('u_ratio'); + }); }); });