Merge pull request #10196 from jahow/webgl-add-string-operators
Webgl / add support for string and arrays in style expressions
This commit is contained in:
@@ -16,12 +16,18 @@ 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]
|
||||
[
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'year'],
|
||||
1850, 0,
|
||||
2015, period
|
||||
]
|
||||
],
|
||||
period
|
||||
],
|
||||
@@ -39,12 +45,15 @@ const style = {
|
||||
symbol: {
|
||||
symbolType: 'circle',
|
||||
size: ['*',
|
||||
['stretch', ['get', 'mass'], 0, 200000, 8, 26],
|
||||
['-', 1.5, ['*', animRatio, 0.5]]
|
||||
['interpolate', ['linear'], ['get', 'mass'], 0, 8, 200000, 26],
|
||||
['-', 1.75, ['*', animRatio, 0.75]]
|
||||
],
|
||||
color: ['interpolate',
|
||||
['linear'],
|
||||
animRatio,
|
||||
newColor, oldColor],
|
||||
0, newColor,
|
||||
1, oldColor
|
||||
],
|
||||
opacity: ['-', 1.0, ['*', animRatio, 0.75]]
|
||||
}
|
||||
};
|
||||
@@ -113,7 +122,8 @@ const map = new Map({
|
||||
}),
|
||||
new WebGLPointsLayer({
|
||||
style: style,
|
||||
source: vectorSource
|
||||
source: vectorSource,
|
||||
disableHitDetection: true
|
||||
})
|
||||
],
|
||||
target: document.getElementById('map'),
|
||||
|
||||
@@ -3,13 +3,13 @@ layout: example.html
|
||||
title: Icon Sprites with WebGL
|
||||
shortdesc: Rendering many icons with WebGL
|
||||
docs: >
|
||||
This example shows how to use `ol/renderer/webgl/PointsLayer` to render
|
||||
This example shows how to use `ol/layer/WebGLPoints` to render
|
||||
a very large amount of sprites. The above map is based on a dataset from the National UFO Reporting Center: each
|
||||
icon marks a UFO sighting according to its reported shape (disk, light, fireball...). The older the sighting, the redder
|
||||
the icon.
|
||||
|
||||
A very simple sprite atlas is used in the form of a PNG file containing all icons on a grid. Then, the `texCoordCallback`
|
||||
option of the `ol/renderer/webgl/PointsLayer` constructor is used to specify which sprite to use according to the sighting shape.
|
||||
A very simple sprite atlas is used in the form of a PNG file containing all icons on a grid. Then, the `style` object
|
||||
given to the `ol/layer/WebGLPoints` constructor is used to specify which sprite to use according to the sighting shape.
|
||||
|
||||
The dataset contains around 80k points and can be found here: https://www.kaggle.com/NUFORC/ufo-sightings
|
||||
tags: "webgl, icon, sprite, point, ufo"
|
||||
|
||||
@@ -4,11 +4,9 @@ import TileLayer from '../src/ol/layer/Tile.js';
|
||||
import TileJSON from '../src/ol/source/TileJSON.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 {formatColor, formatNumber} from '../src/ol/webgl/ShaderBuilder.js';
|
||||
import WebGLPointsLayer from '../src/ol/layer/WebGLPoints.js';
|
||||
|
||||
const key = 'pk.eyJ1IjoidHNjaGF1YiIsImEiOiJjaW5zYW5lNHkxMTNmdWttM3JyOHZtMmNtIn0.CDIBD8H-G2Gf-cPkIuWtRg';
|
||||
|
||||
@@ -17,161 +15,41 @@ const vectorSource = new Vector({
|
||||
attributions: 'National UFO Reporting Center'
|
||||
});
|
||||
|
||||
const texture = new Image();
|
||||
texture.src = 'data/ufo_shapes.png';
|
||||
|
||||
// This describes the content of the associated sprite sheet
|
||||
// coords are u0, v0 for a given shape (all icons have a size of 0.25 x 0.5)
|
||||
const shapeTextureCoords = {
|
||||
'light': [0, 0],
|
||||
'sphere': [0.25, 0],
|
||||
'circle': [0.25, 0],
|
||||
'disc': [0.5, 0],
|
||||
'oval': [0.5, 0],
|
||||
'triangle': [0.75, 0],
|
||||
'fireball': [0, 0.5],
|
||||
'default': [0.75, 0.5]
|
||||
};
|
||||
|
||||
const oldColor = [255, 160, 110];
|
||||
const newColor = [180, 255, 200];
|
||||
const size = 16;
|
||||
|
||||
class WebglPointsLayer extends VectorLayer {
|
||||
createRenderer() {
|
||||
return new WebGLPointsLayerRenderer(this, {
|
||||
attributes: [
|
||||
{
|
||||
name: 'year',
|
||||
callback: function(feature) {
|
||||
return feature.get('year');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'texCoordU',
|
||||
callback: function(feature) {
|
||||
let coords = shapeTextureCoords[feature.get('shape')];
|
||||
if (!coords) {
|
||||
coords = shapeTextureCoords['default'];
|
||||
}
|
||||
return coords[0];
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'texCoordV',
|
||||
callback: function(feature) {
|
||||
let coords = shapeTextureCoords[feature.get('shape')];
|
||||
if (!coords) {
|
||||
coords = shapeTextureCoords['default'];
|
||||
}
|
||||
return coords[1];
|
||||
}
|
||||
}
|
||||
],
|
||||
uniforms: {
|
||||
u_texture: texture
|
||||
},
|
||||
vertexShader: [
|
||||
'precision mediump float;',
|
||||
|
||||
'uniform mat4 u_projectionMatrix;',
|
||||
'uniform mat4 u_offsetScaleMatrix;',
|
||||
'uniform mat4 u_offsetRotateMatrix;',
|
||||
'attribute vec2 a_position;',
|
||||
'attribute float a_index;',
|
||||
'attribute float a_year;',
|
||||
'attribute float a_texCoordU;',
|
||||
'attribute float a_texCoordV;',
|
||||
'varying vec2 v_texCoord;',
|
||||
'varying float v_year;',
|
||||
|
||||
'void main(void) {',
|
||||
' mat4 offsetMatrix = u_offsetScaleMatrix;',
|
||||
' float offsetX = a_index == 0.0 || a_index == 3.0 ? ',
|
||||
' ' + formatNumber(-size / 2) + ' : ' + formatNumber(size / 2) + ';',
|
||||
' float offsetY = a_index == 0.0 || a_index == 1.0 ? ',
|
||||
' ' + formatNumber(-size / 2) + ' : ' + formatNumber(size / 2) + ';',
|
||||
' 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 ? a_texCoordU : a_texCoordU + 0.25;',
|
||||
' float v = a_index == 2.0 || a_index == 3.0 ? a_texCoordV : a_texCoordV + 0.5;',
|
||||
' v_texCoord = vec2(u, v);',
|
||||
' v_year = a_year;',
|
||||
'}'
|
||||
].join(' '),
|
||||
fragmentShader: [
|
||||
'precision mediump float;',
|
||||
|
||||
'uniform float u_time;',
|
||||
'uniform float u_minYear;',
|
||||
'uniform float u_maxYear;',
|
||||
'uniform sampler2D u_texture;',
|
||||
'varying vec2 v_texCoord;',
|
||||
'varying float v_year;',
|
||||
|
||||
'void main(void) {',
|
||||
' vec4 textureColor = texture2D(u_texture, v_texCoord);',
|
||||
' if (textureColor.a < 0.1) {',
|
||||
' discard;',
|
||||
' }',
|
||||
|
||||
// color is interpolated based on year
|
||||
' float ratio = clamp((v_year - 1950.0) / (2013.0 - 1950.0), 0.0, 1.1);',
|
||||
' vec3 color = mix(vec3(' + formatColor(oldColor) + '),',
|
||||
' vec3(' + formatColor(newColor) + '), ratio);',
|
||||
|
||||
' gl_FragColor = vec4(color, 1.0) * textureColor;',
|
||||
' gl_FragColor.rgb *= gl_FragColor.a;',
|
||||
'}'
|
||||
].join(' '),
|
||||
hitVertexShader: [
|
||||
'precision mediump float;',
|
||||
|
||||
'uniform mat4 u_projectionMatrix;',
|
||||
'uniform mat4 u_offsetScaleMatrix;',
|
||||
'uniform mat4 u_offsetRotateMatrix;',
|
||||
'attribute vec2 a_position;',
|
||||
'attribute float a_index;',
|
||||
'attribute vec4 a_hitColor;',
|
||||
'attribute float a_texCoordU;',
|
||||
'attribute float a_texCoordV;',
|
||||
'varying vec2 v_texCoord;',
|
||||
'varying vec4 v_hitColor;',
|
||||
|
||||
'void main(void) {',
|
||||
' mat4 offsetMatrix = u_offsetScaleMatrix;',
|
||||
' float offsetX = a_index == 0.0 || a_index == 3.0 ? ',
|
||||
' ' + formatNumber(-size / 2) + ' : ' + formatNumber(size / 2) + ';',
|
||||
' float offsetY = a_index == 0.0 || a_index == 1.0 ? ',
|
||||
' ' + formatNumber(-size / 2) + ' : ' + formatNumber(size / 2) + ';',
|
||||
' 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 ? a_texCoordU : a_texCoordU + 0.25;',
|
||||
' float v = a_index == 2.0 || a_index == 3.0 ? a_texCoordV : a_texCoordV + 0.5;',
|
||||
' v_texCoord = vec2(u, v);',
|
||||
' v_hitColor = a_hitColor;',
|
||||
'}'
|
||||
].join(' '),
|
||||
hitFragmentShader: [
|
||||
'precision mediump float;',
|
||||
|
||||
'uniform sampler2D u_texture;',
|
||||
'varying vec2 v_texCoord;',
|
||||
'varying vec4 v_hitColor;',
|
||||
|
||||
'void main(void) {',
|
||||
' vec4 textureColor = texture2D(u_texture, v_texCoord);',
|
||||
' if (textureColor.a < 0.1) {',
|
||||
' discard;',
|
||||
' }',
|
||||
|
||||
' gl_FragColor = v_hitColor;',
|
||||
'}'
|
||||
].join(' ')
|
||||
});
|
||||
const style = {
|
||||
symbol: {
|
||||
symbolType: 'image',
|
||||
src: 'data/ufo_shapes.png',
|
||||
size: size,
|
||||
color: [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'year'],
|
||||
1950, oldColor,
|
||||
2013, newColor
|
||||
],
|
||||
rotateWithView: false,
|
||||
offset: [
|
||||
0,
|
||||
9
|
||||
],
|
||||
textureCoord: [
|
||||
'match',
|
||||
['get', 'shape'],
|
||||
'light', [0, 0, 0.25, 0.5],
|
||||
'sphere', [0.25, 0, 0.5, 0.5],
|
||||
'circle', [0.25, 0, 0.5, 0.5],
|
||||
'disc', [0.5, 0, 0.75, 0.5],
|
||||
'oval', [0.5, 0, 0.75, 0.5],
|
||||
'triangle', [0.75, 0, 1, 0.5],
|
||||
'fireball', [0, 0.5, 0.25, 1],
|
||||
[0.75, 0.5, 1, 1]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function loadData() {
|
||||
const client = new XMLHttpRequest();
|
||||
@@ -217,8 +95,9 @@ const map = new Map({
|
||||
crossOrigin: 'anonymous'
|
||||
})
|
||||
}),
|
||||
new WebglPointsLayer({
|
||||
source: vectorSource
|
||||
new WebGLPointsLayer({
|
||||
source: vectorSource,
|
||||
style: style
|
||||
})
|
||||
],
|
||||
target: document.getElementById('map'),
|
||||
@@ -242,7 +121,3 @@ map.on('pointermove', function(evt) {
|
||||
info.innerText = 'On ' + datetime + ', lasted ' + duration + ' seconds and had a "' + shape + '" shape.';
|
||||
});
|
||||
});
|
||||
|
||||
texture.addEventListener('load', function() {
|
||||
map.render();
|
||||
});
|
||||
|
||||
@@ -26,7 +26,9 @@ experimental: true
|
||||
<select id="style-select">
|
||||
<option value="icons">Icons</option>
|
||||
<option value="triangles">Triangles, color related to population</option>
|
||||
<option value="triangles-latitude">Triangles, color related to latitude</option>
|
||||
<option value="circles">Circles, size related to population</option>
|
||||
<option value="circles-zoom">Circles, size related to zoom</option>
|
||||
</select>
|
||||
<textarea style="width: 100%; height: 20rem; font-family: monospace; font-size: small;" id="style-editor"></textarea>
|
||||
<small>
|
||||
|
||||
@@ -28,21 +28,72 @@ const predefinedStyles = {
|
||||
size: 18,
|
||||
color: [
|
||||
'interpolate',
|
||||
['stretch', ['get', 'population'], 20000, 300000, 0, 1],
|
||||
'#5aca5b',
|
||||
'#ff6a19'
|
||||
['linear'],
|
||||
['get', 'population'],
|
||||
20000, '#5aca5b',
|
||||
300000, '#ff6a19'
|
||||
],
|
||||
rotateWithView: true
|
||||
}
|
||||
},
|
||||
'triangles-latitude': {
|
||||
symbol: {
|
||||
symbolType: 'triangle',
|
||||
size: [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'population'],
|
||||
40000, 12,
|
||||
2000000, 24
|
||||
],
|
||||
color: [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'latitude'],
|
||||
-60, '#ff14c3',
|
||||
-20, '#ff621d',
|
||||
20, '#ffed02',
|
||||
60, '#00ff67'
|
||||
],
|
||||
offset: [0, 0],
|
||||
opacity: 0.95
|
||||
}
|
||||
},
|
||||
'circles': {
|
||||
symbol: {
|
||||
symbolType: 'circle',
|
||||
size: ['stretch', ['get', 'population'], 40000, 2000000, 8, 28],
|
||||
size: [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'population'],
|
||||
40000, 8,
|
||||
2000000, 28
|
||||
],
|
||||
color: '#006688',
|
||||
rotateWithView: false,
|
||||
offset: [0, 0],
|
||||
opacity: ['stretch', ['get', 'population'], 40000, 2000000, 0.6, 0.92]
|
||||
opacity: [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'population'],
|
||||
40000, 0.6,
|
||||
2000000, 0.92
|
||||
]
|
||||
}
|
||||
},
|
||||
'circles-zoom': {
|
||||
symbol: {
|
||||
symbolType: 'circle',
|
||||
size: [
|
||||
'interpolate',
|
||||
['exponential', 2.5],
|
||||
['zoom'],
|
||||
2, 1,
|
||||
14, 32
|
||||
],
|
||||
color: '#240572',
|
||||
offset: [0, 0],
|
||||
opacity: 0.95
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -66,7 +117,8 @@ function refreshLayer(newStyle) {
|
||||
const previousLayer = pointsLayer;
|
||||
pointsLayer = new WebGLPointsLayer({
|
||||
source: vectorSource,
|
||||
style: newStyle
|
||||
style: newStyle,
|
||||
disableHitDetection: true
|
||||
});
|
||||
map.addLayer(pointsLayer);
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ import Layer from './Layer.js';
|
||||
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
|
||||
* be visible.
|
||||
* @property {import("../source/Vector.js").default} [source] Source.
|
||||
* @property {boolean} [disableHitDetection] Setting this to true will provide a slight performance boost, but will
|
||||
* prevent all hit detection on the layer.
|
||||
*/
|
||||
|
||||
|
||||
@@ -75,6 +77,12 @@ class WebGLPointsLayer extends Layer {
|
||||
* @type {import('../webgl/ShaderBuilder.js').StyleParseResult}
|
||||
*/
|
||||
this.parseResult_ = parseLiteralStyle(options.style);
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.hitDetectionDisabled_ = !!options.disableHitDetection;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,6 +92,10 @@ class WebGLPointsLayer extends Layer {
|
||||
return new WebGLPointsLayerRenderer(this, {
|
||||
vertexShader: this.parseResult_.builder.getSymbolVertexShader(),
|
||||
fragmentShader: this.parseResult_.builder.getSymbolFragmentShader(),
|
||||
hitVertexShader: !this.hitDetectionDisabled_ &&
|
||||
this.parseResult_.builder.getSymbolVertexShader(true),
|
||||
hitFragmentShader: !this.hitDetectionDisabled_ &&
|
||||
this.parseResult_.builder.getSymbolFragmentShader(true),
|
||||
uniforms: this.parseResult_.uniforms,
|
||||
attributes: this.parseResult_.attributes
|
||||
});
|
||||
|
||||
@@ -147,6 +147,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
||||
options.hitVertexShader
|
||||
);
|
||||
|
||||
if (this.getShaderCompileErrors()) {
|
||||
throw new Error(this.getShaderCompileErrors());
|
||||
}
|
||||
|
||||
const customAttributes = options.attributes ?
|
||||
options.attributes.map(function(attribute) {
|
||||
return {
|
||||
|
||||
@@ -5,49 +5,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 {import("./expressions.js").ExpressionValue} ExpressionValue
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
598
src/ol/style/expressions.js
Normal file
598
src/ol/style/expressions.js
Normal file
@@ -0,0 +1,598 @@
|
||||
/**
|
||||
* Operators and utilities used for style expressions
|
||||
* @module ol/style/expressions
|
||||
*/
|
||||
|
||||
import {asArray, isStringColor} from '../color.js';
|
||||
|
||||
/**
|
||||
* 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`
|
||||
* * `['mod', value1, value1]` returns the result of `value1 % value2` (modulo)
|
||||
* * `['^', value1, value1]` returns the value of `value1` raised to the `value2` power
|
||||
*
|
||||
* * Transform operators:
|
||||
* * `['match', input, match1, output1, ...matchN, outputN, fallback]` compares the `input` value against all
|
||||
* provided `matchX` values, returning the output associated with the first valid match. If no match is found,
|
||||
* returns the `fallback` value.
|
||||
* `input` and `matchX` values must all be of the same type, and can be `number` or `string`. `outputX` and
|
||||
* `fallback` values must be of the same type, and can be of any kind.
|
||||
* * `['interpolate', interpolation, input, stop1, output1, ...stopN, outputN]` returns a value by interpolating between
|
||||
* pairs of inputs and outputs; `interpolation` can either be `['linear']` or `['exponential', base]` where `base` is
|
||||
* the rate of increase from stop A to stop B (i.e. power to which the interpolation ratio is raised); a value
|
||||
* of 1 is equivalent to `['linear']`.
|
||||
* `input` and `stopX` values must all be of type `number`. `outputX` values can be `number` or `color` values.
|
||||
* Note: `input` will be clamped between `stop1` and `stopN`, meaning that all output values will be comprised
|
||||
* between `output1` and `outputN`.
|
||||
*
|
||||
* * 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.
|
||||
*
|
||||
* * Conversion operators:
|
||||
* * `['array', value1, ...valueN]` creates a numerical array from `number` values; please note that the amount of
|
||||
* values can currently only be 2, 3 or 4.
|
||||
* * `['color', red, green, blue, alpha]` creates a `color` value from `number` values; the `alpha` parameter is
|
||||
* optional; if not specified, it will be set to 1.
|
||||
* Note: `red`, `green` and `blue` components must be values between 0 and 255; `alpha` between 0 and 1.
|
||||
*
|
||||
* 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|boolean} ExpressionValue
|
||||
*/
|
||||
|
||||
/**
|
||||
* Possible inferred types from a given value or expression.
|
||||
* Note: these are binary flags.
|
||||
* @enum {number}
|
||||
*/
|
||||
export const ValueTypes = {
|
||||
NUMBER: 0b00001,
|
||||
STRING: 0b00010,
|
||||
COLOR: 0b00100,
|
||||
BOOLEAN: 0b01000,
|
||||
NUMBER_ARRAY: 0b10000,
|
||||
ANY: 0b11111,
|
||||
NONE: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* An operator declaration must contain two methods: `getReturnType` which returns a type based on
|
||||
* the operator arguments, and `toGlsl` which returns a GLSL-compatible string.
|
||||
* Note: both methods can process arguments recursively.
|
||||
* @typedef {Object} Operator
|
||||
* @property {function(Array<ExpressionValue>): ValueTypes|number} getReturnType Returns one or several types
|
||||
* @property {function(ParsingContext, Array<ExpressionValue>, ValueTypes=): string} toGlsl Returns a GLSL-compatible string
|
||||
* Note: takes in an optional type hint as 3rd parameter
|
||||
*/
|
||||
|
||||
/**
|
||||
* Operator declarations
|
||||
* @type {Object<string, Operator>}
|
||||
*/
|
||||
export const Operators = {};
|
||||
|
||||
/**
|
||||
* Returns the possible types for a given value (each type being a binary flag)
|
||||
* To test a value use e.g. `getValueType(v) & ValueTypes.BOOLEAN`
|
||||
* @param {ExpressionValue} value Value
|
||||
* @returns {ValueTypes|number} Type or types inferred from the value
|
||||
*/
|
||||
export function getValueType(value) {
|
||||
if (typeof value === 'number') {
|
||||
return ValueTypes.NUMBER;
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return ValueTypes.BOOLEAN;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
if (isStringColor(value)) {
|
||||
return ValueTypes.COLOR | ValueTypes.STRING;
|
||||
}
|
||||
return ValueTypes.STRING;
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`Unhandled value type: ${JSON.stringify(value)}`);
|
||||
}
|
||||
const valueArr = /** @type {Array<*>} */(value);
|
||||
const onlyNumbers = valueArr.every(function(v) {
|
||||
return typeof v === 'number';
|
||||
});
|
||||
if (onlyNumbers) {
|
||||
if (valueArr.length === 3 || valueArr.length === 4) {
|
||||
return ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY;
|
||||
}
|
||||
return ValueTypes.NUMBER_ARRAY;
|
||||
}
|
||||
if (typeof valueArr[0] !== 'string') {
|
||||
throw new Error(`Expected an expression operator but received: ${JSON.stringify(valueArr)}`);
|
||||
}
|
||||
const operator = Operators[valueArr[0]];
|
||||
if (operator === undefined) {
|
||||
throw new Error(`Unrecognized expression operator: ${JSON.stringify(valueArr)}`);
|
||||
}
|
||||
return operator.getReturnType(valueArr.slice(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if only one value type is enabled in the input number.
|
||||
* @param {ValueTypes|number} valueType Number containing value type binary flags
|
||||
* @return {boolean} True if only one type flag is enabled, false if zero or multiple
|
||||
*/
|
||||
export function isTypeUnique(valueType) {
|
||||
return Math.log2(valueType) % 1 === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context available during the parsing of an expression.
|
||||
* @typedef {Object} ParsingContext
|
||||
* @property {boolean} [inFragmentShader] If false, means the expression output should be made for a vertex shader
|
||||
* @property {Array<string>} variables List of variables used in the expression; contains **unprefixed names**
|
||||
* @property {Array<string>} attributes List of attributes used in the expression; contains **unprefixed names**
|
||||
* @property {Object<string, number>} stringLiteralsMap This object maps all encountered string values to a number
|
||||
*/
|
||||
|
||||
/**
|
||||
* Will return the number as a float with a dot separator, which is required by GLSL.
|
||||
* @param {number} v Numerical value.
|
||||
* @returns {string} The value as string.
|
||||
*/
|
||||
export function numberToGlsl(v) {
|
||||
const s = v.toString();
|
||||
return s.indexOf('.') === -1 ? s + '.0' : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return the number array as a float with a dot separator, concatenated with ', '.
|
||||
* @param {Array<number>} array Numerical values array.
|
||||
* @returns {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
|
||||
*/
|
||||
export function arrayToGlsl(array) {
|
||||
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(numberToGlsl).join(', ')})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 colorToGlsl(color) {
|
||||
const array = asArray(color).slice();
|
||||
if (array.length < 4) {
|
||||
array.push(1);
|
||||
}
|
||||
return arrayToGlsl(
|
||||
array.map(function(c, i) {
|
||||
return i < 3 ? c / 255 : c;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stable equivalent number for the string literal, for use in shaders. This number is then
|
||||
* converted to be a GLSL-compatible string.
|
||||
* @param {ParsingContext} context Parsing context
|
||||
* @param {string} string String literal value
|
||||
* @returns {string} GLSL-compatible string containing a number
|
||||
*/
|
||||
export function stringToGlsl(context, string) {
|
||||
if (context.stringLiteralsMap[string] === undefined) {
|
||||
context.stringLiteralsMap[string] = Object.keys(context.stringLiteralsMap).length;
|
||||
}
|
||||
return numberToGlsl(context.stringLiteralsMap[string]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively parses a style expression and outputs a GLSL-compatible string. Takes in a parsing context that
|
||||
* will be read and modified during the parsing operation.
|
||||
* @param {ParsingContext} context Parsing context
|
||||
* @param {ExpressionValue} value Value
|
||||
* @param {ValueTypes|number} [typeHint] Hint for the expected final type (can be several types combined)
|
||||
* @returns {string} GLSL-compatible output
|
||||
*/
|
||||
export function expressionToGlsl(context, value, typeHint) {
|
||||
// operator
|
||||
if (Array.isArray(value) && typeof value[0] === 'string') {
|
||||
const operator = Operators[value[0]];
|
||||
if (operator === undefined) {
|
||||
throw new Error(`Unrecognized expression operator: ${JSON.stringify(value)}`);
|
||||
}
|
||||
return operator.toGlsl(context, value.slice(1), typeHint);
|
||||
} else if ((getValueType(value) & ValueTypes.NUMBER) > 0) {
|
||||
return numberToGlsl(/** @type {number} */(value));
|
||||
} else if ((getValueType(value) & ValueTypes.BOOLEAN) > 0) {
|
||||
return value.toString();
|
||||
} else if (
|
||||
((getValueType(value) & ValueTypes.STRING) > 0) &&
|
||||
(typeHint === undefined || typeHint == ValueTypes.STRING)
|
||||
) {
|
||||
return stringToGlsl(context, value.toString());
|
||||
} else if (
|
||||
((getValueType(value) & ValueTypes.COLOR) > 0) &&
|
||||
(typeHint === undefined || typeHint == ValueTypes.COLOR)
|
||||
) {
|
||||
return colorToGlsl(/** @type {number[]|string} */(value));
|
||||
} else if ((getValueType(value) & ValueTypes.NUMBER_ARRAY) > 0) {
|
||||
return arrayToGlsl(/** @type {number[]} */(value));
|
||||
}
|
||||
}
|
||||
|
||||
function assertNumber(value) {
|
||||
if (!(getValueType(value) & ValueTypes.NUMBER)) {
|
||||
throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`);
|
||||
}
|
||||
}
|
||||
function assertNumbers(arr) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
assertNumber(arr[i]);
|
||||
}
|
||||
}
|
||||
function assertString(value) {
|
||||
if (!(getValueType(value) & ValueTypes.STRING)) {
|
||||
throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`);
|
||||
}
|
||||
}
|
||||
function assertBoolean(value) {
|
||||
if (!(getValueType(value) & ValueTypes.BOOLEAN)) {
|
||||
throw new Error(`A boolean value was expected, got ${JSON.stringify(value)} instead`);
|
||||
}
|
||||
}
|
||||
function assertArgsCount(args, count) {
|
||||
if (args.length !== count) {
|
||||
throw new Error(`Exactly ${count} arguments were expected, got ${args.length} instead`);
|
||||
}
|
||||
}
|
||||
function assertArgsMinCount(args, count) {
|
||||
if (args.length < count) {
|
||||
throw new Error(`At least ${count} arguments were expected, got ${args.length} instead`);
|
||||
}
|
||||
}
|
||||
function assertArgsMaxCount(args, count) {
|
||||
if (args.length > count) {
|
||||
throw new Error(`At most ${count} arguments were expected, got ${args.length} instead`);
|
||||
}
|
||||
}
|
||||
function assertArgsEven(args) {
|
||||
if (args.length % 2 !== 0) {
|
||||
throw new Error(`An even amount of arguments was expected, got ${args} instead`);
|
||||
}
|
||||
}
|
||||
function assertUniqueInferredType(args, types) {
|
||||
if (!isTypeUnique(types)) {
|
||||
throw new Error(`Could not infer only one type from the following expression: ${JSON.stringify(args)}`);
|
||||
}
|
||||
}
|
||||
|
||||
Operators['get'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.ANY;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 1);
|
||||
assertString(args[0]);
|
||||
const value = args[0].toString();
|
||||
if (context.attributes.indexOf(value) === -1) {
|
||||
context.attributes.push(value);
|
||||
}
|
||||
const prefix = context.inFragmentShader ? 'v_' : 'a_';
|
||||
return prefix + value;
|
||||
}
|
||||
};
|
||||
Operators['var'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.ANY;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 1);
|
||||
assertString(args[0]);
|
||||
const value = args[0].toString();
|
||||
if (context.variables.indexOf(value) === -1) {
|
||||
context.variables.push(value);
|
||||
}
|
||||
return `u_${value}`;
|
||||
}
|
||||
};
|
||||
Operators['time'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 0);
|
||||
return 'u_time';
|
||||
}
|
||||
};
|
||||
Operators['zoom'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 0);
|
||||
return 'u_zoom';
|
||||
}
|
||||
};
|
||||
Operators['resolution'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 0);
|
||||
return 'u_resolution';
|
||||
}
|
||||
};
|
||||
Operators['*'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} * ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['/'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} / ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['+'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} + ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['-'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} - ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['clamp'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 3);
|
||||
assertNumbers(args);
|
||||
const min = expressionToGlsl(context, args[1]);
|
||||
const max = expressionToGlsl(context, args[2]);
|
||||
return `clamp(${expressionToGlsl(context, args[0])}, ${min}, ${max})`;
|
||||
}
|
||||
};
|
||||
Operators['mod'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `mod(${expressionToGlsl(context, args[0])}, ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['^'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `pow(${expressionToGlsl(context, args[0])}, ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['>'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.BOOLEAN;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} > ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['>='] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.BOOLEAN;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} >= ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['<'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.BOOLEAN;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} < ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['<='] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.BOOLEAN;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} <= ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['=='] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.BOOLEAN;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 2);
|
||||
assertNumbers(args);
|
||||
return `(${expressionToGlsl(context, args[0])} == ${expressionToGlsl(context, args[1])})`;
|
||||
}
|
||||
};
|
||||
Operators['!'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.BOOLEAN;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 1);
|
||||
assertBoolean(args[0]);
|
||||
return `(!${expressionToGlsl(context, args[0])})`;
|
||||
}
|
||||
};
|
||||
Operators['between'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.BOOLEAN;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsCount(args, 3);
|
||||
assertNumbers(args);
|
||||
const min = expressionToGlsl(context, args[1]);
|
||||
const max = expressionToGlsl(context, args[2]);
|
||||
const value = expressionToGlsl(context, args[0]);
|
||||
return `(${value} >= ${min} && ${value} <= ${max})`;
|
||||
}
|
||||
};
|
||||
Operators['array'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.NUMBER_ARRAY;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsMinCount(args, 2);
|
||||
assertArgsMaxCount(args, 4);
|
||||
assertNumbers(args);
|
||||
const parsedArgs = args.map(function(val) {
|
||||
return expressionToGlsl(context, val, ValueTypes.NUMBER);
|
||||
});
|
||||
return `vec${args.length}(${parsedArgs.join(', ')})`;
|
||||
}
|
||||
};
|
||||
Operators['color'] = {
|
||||
getReturnType: function(args) {
|
||||
return ValueTypes.COLOR;
|
||||
},
|
||||
toGlsl: function(context, args) {
|
||||
assertArgsMinCount(args, 3);
|
||||
assertArgsMaxCount(args, 4);
|
||||
assertNumbers(args);
|
||||
const array = /** @type {number[]} */(args);
|
||||
if (args.length === 3) {
|
||||
array.push(1);
|
||||
}
|
||||
const parsedArgs = args.map(function(val, i) {
|
||||
return expressionToGlsl(context, val, ValueTypes.NUMBER) + (i < 3 ? ' / 255.0' : '');
|
||||
});
|
||||
return `vec${args.length}(${parsedArgs.join(', ')})`;
|
||||
}
|
||||
};
|
||||
Operators['interpolate'] = {
|
||||
getReturnType: function(args) {
|
||||
let type = ValueTypes.COLOR | ValueTypes.NUMBER;
|
||||
for (let i = 3; i < args.length; i += 2) {
|
||||
type = type & getValueType(args[i]);
|
||||
}
|
||||
return type;
|
||||
},
|
||||
toGlsl: function(context, args, opt_typeHint) {
|
||||
assertArgsEven(args);
|
||||
assertArgsMinCount(args, 6);
|
||||
|
||||
// validate interpolation type
|
||||
const type = args[0];
|
||||
let interpolation;
|
||||
switch (type[0]) {
|
||||
case 'linear': interpolation = 1; break;
|
||||
case 'exponential': interpolation = type[1]; break;
|
||||
default: interpolation = null;
|
||||
}
|
||||
if (!interpolation) {
|
||||
throw new Error(`Invalid interpolation type for "interpolate" operator, received: ${JSON.stringify(type)}`);
|
||||
}
|
||||
|
||||
// compute input/output types
|
||||
const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY;
|
||||
const outputType = Operators['interpolate'].getReturnType(args) & typeHint;
|
||||
assertUniqueInferredType(args, outputType);
|
||||
|
||||
const input = expressionToGlsl(context, args[1]);
|
||||
let result = null;
|
||||
for (let i = 2; i < args.length - 2; i += 2) {
|
||||
const stop1 = expressionToGlsl(context, args[i]);
|
||||
const output1 = expressionToGlsl(context, args[i + 1], outputType);
|
||||
const stop2 = expressionToGlsl(context, args[i + 2]);
|
||||
const output2 = expressionToGlsl(context, args[i + 3], outputType);
|
||||
result = `mix(${result || output1}, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${numberToGlsl(interpolation)}))`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
Operators['match'] = {
|
||||
getReturnType: function(args) {
|
||||
let type = ValueTypes.ANY;
|
||||
for (let i = 2; i < args.length; i += 2) {
|
||||
type = type & getValueType(args[i]);
|
||||
}
|
||||
type = type & getValueType(args[args.length - 1]);
|
||||
return type;
|
||||
},
|
||||
toGlsl: function(context, args, opt_typeHint) {
|
||||
assertArgsEven(args);
|
||||
assertArgsMinCount(args, 4);
|
||||
|
||||
// compute input/output types
|
||||
const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY;
|
||||
const outputType = Operators['match'].getReturnType(args) & typeHint;
|
||||
assertUniqueInferredType(args, outputType);
|
||||
|
||||
const input = expressionToGlsl(context, args[0]);
|
||||
const fallback = expressionToGlsl(context, args[args.length - 1], outputType);
|
||||
let result = null;
|
||||
for (let i = args.length - 3; i >= 1; i -= 2) {
|
||||
const match = expressionToGlsl(context, args[i]);
|
||||
const output = expressionToGlsl(context, args[i + 1], outputType);
|
||||
result = `(${input} == ${match} ? ${output} : ${result || fallback})`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
@@ -43,7 +43,9 @@ export const DefaultUniform = {
|
||||
PROJECTION_MATRIX: 'u_projectionMatrix',
|
||||
OFFSET_SCALE_MATRIX: 'u_offsetScaleMatrix',
|
||||
OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix',
|
||||
TIME: 'u_time'
|
||||
TIME: 'u_time',
|
||||
ZOOM: 'u_zoom',
|
||||
RESOLUTION: 'u_resolution'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -557,6 +559,8 @@ class WebGLHelper extends Disposable {
|
||||
this.setUniformMatrixValue(DefaultUniform.OFFSET_ROTATION_MATRIX, fromTransform(this.tmpMat4_, offsetRotateMatrix));
|
||||
|
||||
this.setUniformFloatValue(DefaultUniform.TIME, (Date.now() - this.startTime_) * 0.001);
|
||||
this.setUniformFloatValue(DefaultUniform.ZOOM, frameState.viewState.zoom);
|
||||
this.setUniformFloatValue(DefaultUniform.RESOLUTION, frameState.viewState.resolution);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,318 +3,7 @@
|
||||
* @module ol/webgl/ShaderBuilder
|
||||
*/
|
||||
|
||||
import {asArray, isStringColor} from '../color.js';
|
||||
|
||||
/**
|
||||
* Will return the number as a float with a dot separator, which is required by GLSL.
|
||||
* @param {number} v Numerical value.
|
||||
* @returns {string} The value as string.
|
||||
*/
|
||||
export function formatNumber(v) {
|
||||
const s = v.toString();
|
||||
return s.indexOf('.') === -1 ? s + '.0' : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return the number array as a float with a dot separator, concatenated with ', '.
|
||||
* @param {Array<number>} array Numerical values array.
|
||||
* @returns {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
|
||||
*/
|
||||
export function formatArray(array) {
|
||||
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 `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(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<string>} 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<string>} 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));
|
||||
}
|
||||
}
|
||||
import {expressionToGlsl, ValueTypes} from '../style/expressions.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} VaryingDescription
|
||||
@@ -559,29 +248,45 @@ export class ShaderBuilder {
|
||||
* 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`
|
||||
*
|
||||
* @param {boolean} [forHitDetection] If true, the shader will be modified to include hit detection variables
|
||||
* (namely, hit color with encoded feature id).
|
||||
* @returns {string} The full shader as a string.
|
||||
*/
|
||||
getSymbolVertexShader() {
|
||||
getSymbolVertexShader(forHitDetection) {
|
||||
const offsetMatrix = this.rotateWithView ?
|
||||
'u_offsetScaleMatrix * u_offsetRotateMatrix' :
|
||||
'u_offsetScaleMatrix';
|
||||
|
||||
let attributes = this.attributes;
|
||||
let varyings = this.varyings;
|
||||
|
||||
if (forHitDetection) {
|
||||
attributes = attributes.concat('vec4 a_hitColor');
|
||||
varyings = varyings.concat({
|
||||
name: 'v_hitColor',
|
||||
type: 'vec4',
|
||||
expression: 'a_hitColor'
|
||||
});
|
||||
}
|
||||
|
||||
return `precision mediump float;
|
||||
uniform mat4 u_projectionMatrix;
|
||||
uniform mat4 u_offsetScaleMatrix;
|
||||
uniform mat4 u_offsetRotateMatrix;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
${this.uniforms.map(function(uniform) {
|
||||
return 'uniform ' + uniform + ';';
|
||||
}).join('\n')}
|
||||
attribute vec2 a_position;
|
||||
attribute float a_index;
|
||||
${this.attributes.map(function(attribute) {
|
||||
${attributes.map(function(attribute) {
|
||||
return 'attribute ' + attribute + ';';
|
||||
}).join('\n')}
|
||||
varying vec2 v_texCoord;
|
||||
varying vec2 v_quadCoord;
|
||||
${this.varyings.map(function(varying) {
|
||||
${varyings.map(function(varying) {
|
||||
return 'varying ' + varying.type + ' ' + varying.name + ';';
|
||||
}).join('\n')}
|
||||
void main(void) {
|
||||
@@ -593,13 +298,13 @@ void main(void) {
|
||||
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
|
||||
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
|
||||
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;
|
||||
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.p;
|
||||
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.q;
|
||||
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);
|
||||
${this.varyings.map(function(varying) {
|
||||
${varyings.map(function(varying) {
|
||||
return ' ' + varying.name + ' = ' + varying.expression + ';';
|
||||
}).join('\n')}
|
||||
}`;
|
||||
@@ -612,23 +317,41 @@ ${this.varyings.map(function(varying) {
|
||||
* Expects the following varyings to be transmitted by the vertex shader:
|
||||
* `vec2 v_quadCoord`, `vec2 v_texCoord`
|
||||
*
|
||||
* @param {boolean} [forHitDetection] If true, the shader will be modified to include hit detection variables
|
||||
* (namely, hit color with encoded feature id).
|
||||
* @returns {string} The full shader as a string.
|
||||
*/
|
||||
getSymbolFragmentShader() {
|
||||
getSymbolFragmentShader(forHitDetection) {
|
||||
const hitDetectionBypass = forHitDetection ?
|
||||
' if (gl_FragColor.a < 0.1) { discard; } gl_FragColor = v_hitColor;' : '';
|
||||
|
||||
let varyings = this.varyings;
|
||||
|
||||
if (forHitDetection) {
|
||||
varyings = varyings.concat({
|
||||
name: 'v_hitColor',
|
||||
type: 'vec4',
|
||||
expression: 'a_hitColor'
|
||||
});
|
||||
}
|
||||
|
||||
return `precision mediump float;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
${this.uniforms.map(function(uniform) {
|
||||
return 'uniform ' + uniform + ';';
|
||||
}).join('\n')}
|
||||
varying vec2 v_texCoord;
|
||||
varying vec2 v_quadCoord;
|
||||
${this.varyings.map(function(varying) {
|
||||
${varyings.map(function(varying) {
|
||||
return 'varying ' + varying.type + ' ' + varying.name + ';';
|
||||
}).join('\n')}
|
||||
void main(void) {
|
||||
if (${this.discardExpression}) { discard; }
|
||||
gl_FragColor = ${this.colorExpression};
|
||||
gl_FragColor.rgb *= gl_FragColor.a;
|
||||
${hitDetectionBypass}
|
||||
}`;
|
||||
}
|
||||
}
|
||||
@@ -653,28 +376,39 @@ void main(void) {
|
||||
*/
|
||||
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 size = symbStyle.size !== undefined ? symbStyle.size : 1;
|
||||
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;
|
||||
|
||||
const variables = [];
|
||||
const vertAttributes = [];
|
||||
// parse function for vertex shader
|
||||
function pVert(value) {
|
||||
return parse(value, vertAttributes, 'a_', variables);
|
||||
}
|
||||
/**
|
||||
* @type {import("../style/expressions.js").ParsingContext}
|
||||
*/
|
||||
const vertContext = {
|
||||
inFragmentShader: false,
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {}
|
||||
};
|
||||
const parsedSize = expressionToGlsl(vertContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER);
|
||||
const parsedOffset = expressionToGlsl(vertContext, offset, ValueTypes.NUMBER_ARRAY);
|
||||
const parsedTexCoord = expressionToGlsl(vertContext, texCoord, ValueTypes.NUMBER_ARRAY);
|
||||
|
||||
const fragAttributes = [];
|
||||
// parse function for fragment shader
|
||||
function pFrag(value, type) {
|
||||
return parse(value, fragAttributes, 'v_', variables, type);
|
||||
}
|
||||
/**
|
||||
* @type {import("../style/expressions.js").ParsingContext}
|
||||
*/
|
||||
const fragContext = {
|
||||
inFragmentShader: true,
|
||||
variables: vertContext.variables,
|
||||
attributes: [],
|
||||
stringLiteralsMap: vertContext.stringLiteralsMap
|
||||
};
|
||||
const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR);
|
||||
const parsedOpacity = expressionToGlsl(fragContext, opacity, ValueTypes.NUMBER);
|
||||
|
||||
let opacityFilter = '1.0';
|
||||
const visibleSize = pFrag(size[0]);
|
||||
const visibleSize = `vec2(${expressionToGlsl(fragContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER)}).x`;
|
||||
switch (symbStyle.symbolType) {
|
||||
case 'square': break;
|
||||
case 'image': break;
|
||||
@@ -691,26 +425,24 @@ export function parseLiteralStyle(style) {
|
||||
default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType);
|
||||
}
|
||||
|
||||
const parsedColor = pFrag(color, ValueTypes.COLOR);
|
||||
|
||||
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])})`)
|
||||
.setSizeExpression(`vec2(${parsedSize})`)
|
||||
.setSymbolOffsetExpression(parsedOffset)
|
||||
.setTextureCoordinateExpression(parsedTexCoord)
|
||||
.setSymbolRotateWithView(!!symbStyle.rotateWithView)
|
||||
.setColorExpression(
|
||||
`vec4(${parsedColor}.rgb, ${parsedColor}.a * ${pFrag(opacity)} * ${opacityFilter})`);
|
||||
`vec4(${parsedColor}.rgb, ${parsedColor}.a * ${parsedOpacity} * ${opacityFilter})`);
|
||||
|
||||
if (style.filter) {
|
||||
builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`);
|
||||
const parsedFilter = expressionToGlsl(fragContext, style.filter, ValueTypes.BOOLEAN);
|
||||
builder.setFragmentDiscardExpression(`!${parsedFilter}`);
|
||||
}
|
||||
|
||||
/** @type {Object.<string,import("../webgl/Helper").UniformValue>} */
|
||||
const uniforms = {};
|
||||
|
||||
// define one uniform per variable
|
||||
variables.forEach(function(varName) {
|
||||
fragContext.variables.forEach(function(varName) {
|
||||
builder.addUniform(`float u_${varName}`);
|
||||
uniforms[`u_${varName}`] = function() {
|
||||
return style.variables && style.variables[varName] !== undefined ?
|
||||
@@ -729,25 +461,29 @@ export function parseLiteralStyle(style) {
|
||||
|
||||
// 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);
|
||||
fragContext.attributes.forEach(function(attrName) {
|
||||
if (vertContext.attributes.indexOf(attrName) === -1) {
|
||||
vertContext.attributes.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) {
|
||||
vertContext.attributes.forEach(function(attrName) {
|
||||
builder.addAttribute(`float a_${attrName}`);
|
||||
});
|
||||
|
||||
return {
|
||||
builder: builder,
|
||||
attributes: vertAttributes.map(function(attributeName) {
|
||||
attributes: vertContext.attributes.map(function(attributeName) {
|
||||
return {
|
||||
name: attributeName,
|
||||
callback: function(feature) {
|
||||
return feature.get(attributeName) || 0;
|
||||
let value = feature.get(attributeName);
|
||||
if (typeof value === 'string') {
|
||||
value = vertContext.stringLiteralsMap[value];
|
||||
}
|
||||
return value !== undefined ? value : -9999999; // to avoid matching with the first string literal
|
||||
}
|
||||
};
|
||||
}),
|
||||
|
||||
561
test/spec/ol/style/expressions.test.js
Normal file
561
test/spec/ol/style/expressions.test.js
Normal file
@@ -0,0 +1,561 @@
|
||||
import {
|
||||
arrayToGlsl, colorToGlsl,
|
||||
expressionToGlsl,
|
||||
getValueType, isTypeUnique,
|
||||
numberToGlsl, stringToGlsl,
|
||||
ValueTypes
|
||||
} from '../../../../src/ol/style/expressions.js';
|
||||
|
||||
|
||||
describe('ol.style.expressions', function() {
|
||||
|
||||
describe('numberToGlsl', function() {
|
||||
it('does a simple transform when a fraction is present', function() {
|
||||
expect(numberToGlsl(1.3456)).to.eql('1.3456');
|
||||
});
|
||||
it('adds a fraction separator when missing', function() {
|
||||
expect(numberToGlsl(1)).to.eql('1.0');
|
||||
expect(numberToGlsl(2.0)).to.eql('2.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayToGlsl', function() {
|
||||
it('outputs numbers with dot separators', function() {
|
||||
expect(arrayToGlsl([1, 0, 3.45, 0.8888])).to.eql('vec4(1.0, 0.0, 3.45, 0.8888)');
|
||||
expect(arrayToGlsl([3, 4])).to.eql('vec2(3.0, 4.0)');
|
||||
});
|
||||
it('throws on invalid lengths', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
arrayToGlsl([3]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
try {
|
||||
arrayToGlsl([3, 2, 1, 0, -1]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('colorToGlsl', function() {
|
||||
it('normalizes color and outputs numbers with dot separators', function() {
|
||||
expect(colorToGlsl([100, 0, 255])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
|
||||
expect(colorToGlsl([100, 0, 255, 1])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
|
||||
});
|
||||
it('handles colors in string format', function() {
|
||||
expect(colorToGlsl('red')).to.eql('vec4(1.0, 0.0, 0.0, 1.0)');
|
||||
expect(colorToGlsl('#00ff99')).to.eql('vec4(0.0, 1.0, 0.6, 1.0)');
|
||||
expect(colorToGlsl('rgb(100, 0, 255)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
|
||||
expect(colorToGlsl('rgba(100, 0, 255, 0.3)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 0.3)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringToGlsl', function() {
|
||||
let context;
|
||||
beforeEach(function() {
|
||||
context = {
|
||||
stringLiteralsMap: {}
|
||||
};
|
||||
});
|
||||
|
||||
it('maps input string to stable numbers', function() {
|
||||
expect(stringToGlsl(context, 'abcd')).to.eql('0.0');
|
||||
expect(stringToGlsl(context, 'defg')).to.eql('1.0');
|
||||
expect(stringToGlsl(context, 'hijk')).to.eql('2.0');
|
||||
expect(stringToGlsl(context, 'abcd')).to.eql('0.0');
|
||||
expect(stringToGlsl(context, 'def')).to.eql('3.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTypeUnique', function() {
|
||||
it('return true if only one value type', function() {
|
||||
expect(isTypeUnique(ValueTypes.NUMBER)).to.eql(true);
|
||||
expect(isTypeUnique(ValueTypes.STRING)).to.eql(true);
|
||||
expect(isTypeUnique(ValueTypes.COLOR)).to.eql(true);
|
||||
});
|
||||
it('return false if several value types', function() {
|
||||
expect(isTypeUnique(ValueTypes.NUMBER | ValueTypes.COLOR)).to.eql(false);
|
||||
expect(isTypeUnique(ValueTypes.ANY)).to.eql(false);
|
||||
});
|
||||
it('return false if no value type', function() {
|
||||
expect(isTypeUnique(ValueTypes.NUMBER & ValueTypes.COLOR)).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValueType', function() {
|
||||
|
||||
it('correctly analyzes a literal value', function() {
|
||||
expect(getValueType(1234)).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType([1, 2, 3, 4])).to.eql(ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY);
|
||||
expect(getValueType([1, 2, 3])).to.eql(ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY);
|
||||
expect(getValueType([1, 2])).to.eql(ValueTypes.NUMBER_ARRAY);
|
||||
expect(getValueType([1, 2, 3, 4, 5])).to.eql(ValueTypes.NUMBER_ARRAY);
|
||||
expect(getValueType('yellow')).to.eql(ValueTypes.COLOR | ValueTypes.STRING);
|
||||
expect(getValueType('#113366')).to.eql(ValueTypes.COLOR | ValueTypes.STRING);
|
||||
expect(getValueType('rgba(252,171,48,0.62)')).to.eql(ValueTypes.COLOR | ValueTypes.STRING);
|
||||
expect(getValueType('abcd')).to.eql(ValueTypes.STRING);
|
||||
expect(getValueType(true)).to.eql(ValueTypes.BOOLEAN);
|
||||
});
|
||||
|
||||
it('throws on an unsupported type (object)', function(done) {
|
||||
try {
|
||||
getValueType(new Object());
|
||||
} catch (e) {
|
||||
done();
|
||||
}
|
||||
done(true);
|
||||
});
|
||||
|
||||
it('throws on an unsupported type (mixed array)', function(done) {
|
||||
try {
|
||||
getValueType([1, true, 'aa']);
|
||||
} catch (e) {
|
||||
done();
|
||||
}
|
||||
done(true);
|
||||
});
|
||||
|
||||
it('correctly analyzes operator return types', function() {
|
||||
expect(getValueType(['get', 'myAttr'])).to.eql(ValueTypes.ANY);
|
||||
expect(getValueType(['var', 'myValue'])).to.eql(ValueTypes.ANY);
|
||||
expect(getValueType(['time'])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['zoom'])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['resolution'])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['+', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['-', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['/', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['*', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['^', 10, 2])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['mod', ['time'], 10])).to.eql(ValueTypes.NUMBER);
|
||||
expect(getValueType(['>', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['>=', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['<', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['<=', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['==', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['!', ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['array', ['get', 'attr4'], 1, 2, 3])).to.eql(ValueTypes.NUMBER_ARRAY);
|
||||
expect(getValueType(['color', ['get', 'attr4'], 1, 2])).to.eql(ValueTypes.COLOR);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('expressionToGlsl', function() {
|
||||
let context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {}
|
||||
};
|
||||
});
|
||||
|
||||
it('correctly converts expressions to GLSL', function() {
|
||||
expect(expressionToGlsl(context, ['get', 'myAttr'])).to.eql('a_myAttr');
|
||||
expect(expressionToGlsl(context, ['var', 'myValue'])).to.eql('u_myValue');
|
||||
expect(expressionToGlsl(context, ['time'])).to.eql('u_time');
|
||||
expect(expressionToGlsl(context, ['zoom'])).to.eql('u_zoom');
|
||||
expect(expressionToGlsl(context, ['resolution'])).to.eql('u_resolution');
|
||||
expect(expressionToGlsl(context, ['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)');
|
||||
expect(expressionToGlsl(context, ['/', ['-', ['get', 'size'], 20], 100])).to.eql('((a_size - 20.0) / 100.0)');
|
||||
expect(expressionToGlsl(context, ['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)');
|
||||
expect(expressionToGlsl(context, ['^', ['mod', ['time'], 10], 2])).to.eql('pow(mod(u_time, 10.0), 2.0)');
|
||||
expect(expressionToGlsl(context, ['>', 10, ['get', 'attr4']])).to.eql('(10.0 > a_attr4)');
|
||||
expect(expressionToGlsl(context, ['>=', 10, ['get', 'attr4']])).to.eql('(10.0 >= a_attr4)');
|
||||
expect(expressionToGlsl(context, ['<', 10, ['get', 'attr4']])).to.eql('(10.0 < a_attr4)');
|
||||
expect(expressionToGlsl(context, ['<=', 10, ['get', 'attr4']])).to.eql('(10.0 <= a_attr4)');
|
||||
expect(expressionToGlsl(context, ['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4)');
|
||||
expect(expressionToGlsl(context, ['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0)');
|
||||
expect(expressionToGlsl(context, ['!', ['get', 'attr4']])).to.eql('(!a_attr4)');
|
||||
expect(expressionToGlsl(context, ['array', ['get', 'attr4'], 1, 2, 3])).to.eql('vec4(a_attr4, 1.0, 2.0, 3.0)');
|
||||
expect(expressionToGlsl(context, ['color', ['get', 'attr4'], 1, 2, 0.5])).to.eql('vec4(a_attr4 / 255.0, 1.0 / 255.0, 2.0 / 255.0, 0.5)');
|
||||
});
|
||||
|
||||
it('correctly adapts output for fragment shaders', function() {
|
||||
context.inFragmentShader = true;
|
||||
expect(expressionToGlsl(context, ['get', 'myAttr'])).to.eql('v_myAttr');
|
||||
});
|
||||
|
||||
it('correctly adapts output for fragment shaders', function() {
|
||||
expressionToGlsl(context, ['get', 'myAttr']);
|
||||
expressionToGlsl(context, ['var', 'myVar']);
|
||||
expressionToGlsl(context, ['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]);
|
||||
expressionToGlsl(context, ['*', ['get', 'attr2'], ['var', 'myVar']]);
|
||||
expressionToGlsl(context, ['*', ['get', 'attr3'], ['var', 'myVar2']]);
|
||||
expect(context.attributes).to.eql(['myAttr', 'attr2', 'attr3']);
|
||||
expect(context.variables).to.eql(['myVar', 'myVar2']);
|
||||
});
|
||||
|
||||
it('gives precedence to the string type unless asked otherwise', function() {
|
||||
expect(expressionToGlsl(context, 'lightgreen')).to.eql('0.0');
|
||||
expect(expressionToGlsl(context, 'lightgreen', ValueTypes.COLOR)).to.eql(
|
||||
'vec4(0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1.0)');
|
||||
});
|
||||
|
||||
it('throws on unsupported types for operators', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['var', 1234]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['<', 0, 'aa']);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['+', true, ['get', 'attr']]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['color', 1, 2, 'red']);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['array', 1, '2', 3]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
|
||||
it('throws with the wrong number of arguments', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['var', 1234, 456]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['<', 4]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['+']);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['array', 1]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['color', 1, 2, 3, 4, 5]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
|
||||
it('throws on invalid expressions', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, null);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('match operator', function() {
|
||||
let context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {}
|
||||
};
|
||||
});
|
||||
|
||||
it('correctly guesses the output type', function() {
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, 'red', 1, 'yellow', 'green']))
|
||||
.to.eql(ValueTypes.STRING | ValueTypes.COLOR);
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, 'not_a_color', 1, 'yellow', 'green']))
|
||||
.to.eql(ValueTypes.STRING);
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, 'red', 1, 'yellow', 'not_a_color']))
|
||||
.to.eql(ValueTypes.STRING);
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, [1, 1, 0], 1, [1, 0, 1], [0, 1, 1]]))
|
||||
.to.eql(ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY);
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, [1, 1, 0], 1, [1, 0, 1], 'white']))
|
||||
.to.eql(ValueTypes.COLOR);
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, 'red', 1, true, 100]))
|
||||
.to.eql(ValueTypes.NONE);
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, false, 1, true, false]))
|
||||
.to.eql(ValueTypes.BOOLEAN);
|
||||
expect(getValueType(['match', ['get', 'attr'], 0, 100, 1, 200, 300]))
|
||||
.to.eql(ValueTypes.NUMBER);
|
||||
});
|
||||
|
||||
it('throws if no single output type could be inferred', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['match', ['get', 'attr'], 0, 'red', 1, 'yellow', 'green'], ValueTypes.COLOR);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(false);
|
||||
|
||||
try {
|
||||
expressionToGlsl(context, ['match', ['get', 'attr'], 0, 'red', 1, 'yellow', 'green']);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['match', ['get', 'attr'], 0, 'red', 1, 'yellow', 'green'], ValueTypes.NUMBER);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
|
||||
it('throws if invalid argument count', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['match', ['get', 'attr'], 0, true, false, false]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['match', ['get', 'attr'], 0, true]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
try {
|
||||
expressionToGlsl(context, ['match', ['get', 'attr'], 0]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
|
||||
it('correctly parses the expression (colors)', function() {
|
||||
expect(expressionToGlsl(context, ['match', ['get', 'attr'], 0, 'red', 1, 'yellow', 'white'], ValueTypes.COLOR))
|
||||
.to.eql('(a_attr == 0.0 ? vec4(1.0, 0.0, 0.0, 1.0) : (a_attr == 1.0 ? vec4(1.0, 1.0, 0.0, 1.0) : vec4(1.0, 1.0, 1.0, 1.0)))');
|
||||
});
|
||||
|
||||
it('correctly parses the expression (strings)', function() {
|
||||
function toGlsl(string) {
|
||||
return stringToGlsl(context, string);
|
||||
}
|
||||
expect(expressionToGlsl(context, ['match', ['get', 'attr'], 10, 'red', 20, 'yellow', 'white'], ValueTypes.STRING))
|
||||
.to.eql(`(a_attr == 10.0 ? ${toGlsl('red')} : (a_attr == 20.0 ? ${toGlsl('yellow')} : ${toGlsl('white')}))`);
|
||||
});
|
||||
|
||||
it('correctly parses the expression (number arrays)', function() {
|
||||
function toGlsl(string) {
|
||||
return stringToGlsl(context, string);
|
||||
}
|
||||
expect(expressionToGlsl(context, ['match', ['get', 'attr'], 'low', [0, 0], 'high', [0, 1], [1, 0]]))
|
||||
.to.eql(`(a_attr == ${toGlsl('low')} ? vec2(0.0, 0.0) : (a_attr == ${toGlsl('high')} ? vec2(0.0, 1.0) : vec2(1.0, 0.0)))`);
|
||||
expect(expressionToGlsl(context, ['match', ['get', 'attr'], 0, [0, 0, 1, 1], 1, [1, 1, 2, 2], 2, [2, 2, 3, 3], [3, 3, 4, 4]], ValueTypes.NUMBER_ARRAY))
|
||||
.to.eql('(a_attr == 0.0 ? vec4(0.0, 0.0, 1.0, 1.0) : (a_attr == 1.0 ? vec4(1.0, 1.0, 2.0, 2.0) : (a_attr == 2.0 ? vec4(2.0, 2.0, 3.0, 3.0) : vec4(3.0, 3.0, 4.0, 4.0))))');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('interpolate operator', function() {
|
||||
let context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {}
|
||||
};
|
||||
});
|
||||
|
||||
it('correctly guesses the output type', function() {
|
||||
expect(getValueType(['interpolate', ['linear'], ['get', 'attr'], 0, 'red', 100, 'yellow']))
|
||||
.to.eql(ValueTypes.COLOR);
|
||||
expect(getValueType(['interpolate', ['linear'], ['get', 'attr'], 0, [1, 2, 3], 1, [0, 0, 0, 4]]))
|
||||
.to.eql(ValueTypes.COLOR);
|
||||
expect(getValueType(['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 10]))
|
||||
.to.eql(ValueTypes.NUMBER);
|
||||
});
|
||||
|
||||
it('throws if no single output type could be inferred', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 10], ValueTypes.COLOR);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 0, [1, 2, 3], 1, 222]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 0, [1, 2, 3], 1, [0, 0, 0, 4]], ValueTypes.NUMBER);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
|
||||
it('throws if invalid argument count', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 1000]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 10, 5000]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
|
||||
it('throws if an invalid interpolation type is given', function() {
|
||||
let thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', 'linear', ['get', 'attr'], 1000, 0, 2000, 1]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', ['exponential'], ['get', 'attr'], 1000, -10, 2000, 1]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
|
||||
thrown = false;
|
||||
try {
|
||||
expressionToGlsl(context, ['interpolate', ['not_a_type'], ['get', 'attr'], 1000, -10, 2000, 1]);
|
||||
} catch (e) {
|
||||
thrown = true;
|
||||
}
|
||||
expect(thrown).to.be(true);
|
||||
});
|
||||
|
||||
it('correctly parses the expression (colors, linear)', function() {
|
||||
expect(expressionToGlsl(context,
|
||||
['interpolate', ['linear'], ['get', 'attr'], 1000, [255, 0, 0], 2000, [0, 255, 0]]
|
||||
)).to.eql(
|
||||
'mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0))');
|
||||
expect(expressionToGlsl(context,
|
||||
['interpolate', ['linear'], ['get', 'attr'], 1000, [255, 0, 0], 2000, [0, 255, 0], 5000, [0, 0, 255]]
|
||||
)).to.eql(
|
||||
'mix(mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0)), vec4(0.0, 0.0, 1.0, 1.0), pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 1.0))');
|
||||
});
|
||||
|
||||
it('correctly parses the expression (number, linear)', function() {
|
||||
expect(expressionToGlsl(context,
|
||||
['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 0, 5000, 10]
|
||||
)).to.eql(
|
||||
'mix(mix(-10.0, 0.0, pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0)), 10.0, pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 1.0))');
|
||||
});
|
||||
|
||||
it('correctly parses the expression (number, exponential)', function() {
|
||||
expect(expressionToGlsl(context,
|
||||
['interpolate', ['exponential', 0.5], ['get', 'attr'], 1000, -10, 2000, 0, 5000, 10]
|
||||
)).to.eql(
|
||||
'mix(mix(-10.0, 0.0, pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 0.5)), 10.0, pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 0.5))');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('complex expressions', function() {
|
||||
let context;
|
||||
|
||||
beforeEach(function() {
|
||||
context = {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {}
|
||||
};
|
||||
});
|
||||
|
||||
it('correctly parses a combination of interpolate, match, color and number', function() {
|
||||
const expression = ['interpolate',
|
||||
['linear'],
|
||||
['^',
|
||||
['/',
|
||||
['mod',
|
||||
['+',
|
||||
['time'],
|
||||
[
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'year'],
|
||||
1850, 0,
|
||||
2015, 8
|
||||
]
|
||||
],
|
||||
8
|
||||
],
|
||||
8
|
||||
],
|
||||
0.5
|
||||
],
|
||||
0, 'rgba(255, 255, 0, 0.5)',
|
||||
1, ['match',
|
||||
['get', 'year'],
|
||||
2000, 'green',
|
||||
'#ffe52c'
|
||||
]
|
||||
];
|
||||
expect(expressionToGlsl(context, expression)).to.eql(
|
||||
'mix(vec4(1.0, 1.0, 0.0, 0.5), (a_year == 2000.0 ? vec4(0.0, 0.5019607843137255, 0.0, 1.0) : vec4(1.0, 0.8980392156862745, 0.17254901960784313, 1.0)), pow(clamp((pow((mod((u_time + mix(0.0, 8.0, pow(clamp((a_year - 1850.0) / (2015.0 - 1850.0), 0.0, 1.0), 1.0))), 8.0) / 8.0), 0.5) - 0.0) / (1.0 - 0.0), 0.0, 1.0), 1.0))'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,8 @@ const VERTEX_SHADER = `
|
||||
uniform mat4 u_offsetScaleMatrix;
|
||||
uniform mat4 u_offsetRotateMatrix;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
|
||||
attribute float a_test;
|
||||
uniform float u_test;
|
||||
@@ -28,6 +30,8 @@ const INVALID_VERTEX_SHADER = `
|
||||
uniform mat4 u_offsetScaleMatrix;
|
||||
uniform mat4 u_offsetRotateMatrix;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
|
||||
bla
|
||||
uniform float u_test;
|
||||
|
||||
@@ -1,118 +1,25 @@
|
||||
import {
|
||||
check,
|
||||
formatArray,
|
||||
formatColor,
|
||||
formatNumber,
|
||||
isValueTypeColor,
|
||||
isValueTypeNumber,
|
||||
isValueTypeString,
|
||||
parse,
|
||||
parseLiteralStyle,
|
||||
ShaderBuilder,
|
||||
ValueTypes
|
||||
} from '../../../../src/ol/webgl/ShaderBuilder.js';
|
||||
import {parseLiteralStyle, ShaderBuilder} from '../../../../src/ol/webgl/ShaderBuilder.js';
|
||||
import {arrayToGlsl, colorToGlsl, numberToGlsl} from '../../../../src/ol/style/expressions.js';
|
||||
|
||||
describe('ol.webgl.ShaderBuilder', function() {
|
||||
|
||||
describe('formatNumber', function() {
|
||||
it('does a simple transform when a fraction is present', function() {
|
||||
expect(formatNumber(1.3456)).to.eql('1.3456');
|
||||
});
|
||||
it('adds a fraction separator when missing', function() {
|
||||
expect(formatNumber(1)).to.eql('1.0');
|
||||
expect(formatNumber(2.0)).to.eql('2.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatArray', function() {
|
||||
it('outputs numbers with dot separators', function() {
|
||||
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])).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 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]));
|
||||
builder.addVarying('v_opacity', 'float', numberToGlsl(0.4));
|
||||
builder.addVarying('v_test', 'vec3', arrayToGlsl([1, 2, 3]));
|
||||
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||
builder.setColorExpression(colorToGlsl([80, 0, 255, 1]));
|
||||
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||
|
||||
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_zoom;
|
||||
uniform float u_resolution;
|
||||
|
||||
attribute vec2 a_position;
|
||||
attribute float a_index;
|
||||
@@ -130,8 +37,8 @@ void main(void) {
|
||||
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
|
||||
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
|
||||
vec4 texCoord = vec4(0.0, 0.5, 0.5, 1.0);
|
||||
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;
|
||||
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.p;
|
||||
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.q;
|
||||
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;
|
||||
@@ -144,16 +51,18 @@ void main(void) {
|
||||
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]));
|
||||
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||
builder.setColorExpression(colorToGlsl([80, 0, 255, 1]));
|
||||
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||
|
||||
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_zoom;
|
||||
uniform float u_resolution;
|
||||
uniform float u_myUniform;
|
||||
attribute vec2 a_position;
|
||||
attribute float a_index;
|
||||
@@ -170,8 +79,8 @@ void main(void) {
|
||||
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
|
||||
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
|
||||
vec4 texCoord = vec4(0.0, 0.5, 0.5, 1.0);
|
||||
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;
|
||||
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.p;
|
||||
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.q;
|
||||
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;
|
||||
@@ -181,10 +90,10 @@ void main(void) {
|
||||
});
|
||||
it('generates a symbol vertex shader (with rotateWithView)', function() {
|
||||
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.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||
builder.setColorExpression(colorToGlsl([80, 0, 255, 1]));
|
||||
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||
builder.setSymbolRotateWithView(true);
|
||||
|
||||
expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
|
||||
@@ -192,6 +101,8 @@ uniform mat4 u_projectionMatrix;
|
||||
uniform mat4 u_offsetScaleMatrix;
|
||||
uniform mat4 u_offsetRotateMatrix;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
|
||||
attribute vec2 a_position;
|
||||
attribute float a_index;
|
||||
@@ -208,13 +119,48 @@ void main(void) {
|
||||
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
|
||||
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
|
||||
vec4 texCoord = vec4(0.0, 0.5, 0.5, 1.0);
|
||||
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;
|
||||
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.p;
|
||||
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.q;
|
||||
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);
|
||||
|
||||
}`);
|
||||
});
|
||||
it('generates a symbol vertex shader for hitDetection', function() {
|
||||
const builder = new ShaderBuilder();
|
||||
|
||||
expect(builder.getSymbolVertexShader(true)).to.eql(`precision mediump float;
|
||||
uniform mat4 u_projectionMatrix;
|
||||
uniform mat4 u_offsetScaleMatrix;
|
||||
uniform mat4 u_offsetRotateMatrix;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
|
||||
attribute vec2 a_position;
|
||||
attribute float a_index;
|
||||
attribute vec4 a_hitColor;
|
||||
varying vec2 v_texCoord;
|
||||
varying vec2 v_quadCoord;
|
||||
varying vec4 v_hitColor;
|
||||
void main(void) {
|
||||
mat4 offsetMatrix = u_offsetScaleMatrix;
|
||||
vec2 size = vec2(1.0);
|
||||
vec2 offset = vec2(0.0);
|
||||
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 = vec4(0.0, 0.0, 1.0, 1.0);
|
||||
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.p;
|
||||
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.q;
|
||||
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);
|
||||
v_hitColor = a_hitColor;
|
||||
}`);
|
||||
});
|
||||
});
|
||||
@@ -222,15 +168,17 @@ void main(void) {
|
||||
describe('getSymbolFragmentShader', function() {
|
||||
it('generates a symbol fragment shader (with varying)', function() {
|
||||
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]));
|
||||
builder.addVarying('v_opacity', 'float', numberToGlsl(0.4));
|
||||
builder.addVarying('v_test', 'vec3', arrayToGlsl([1, 2, 3]));
|
||||
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||
builder.setColorExpression(colorToGlsl([80, 0, 255]));
|
||||
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||
|
||||
expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
varying vec2 v_quadCoord;
|
||||
@@ -240,20 +188,23 @@ void main(void) {
|
||||
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 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.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||
builder.setColorExpression(colorToGlsl([255, 255, 255, 1]));
|
||||
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||
builder.setFragmentDiscardExpression('u_myUniform > 0.5');
|
||||
|
||||
expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
uniform float u_myUniform;
|
||||
uniform vec2 u_myUniform2;
|
||||
varying vec2 v_texCoord;
|
||||
@@ -263,155 +214,26 @@ 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;
|
||||
|
||||
}`);
|
||||
});
|
||||
});
|
||||
it('generates a symbol fragment shader for hit detection', function() {
|
||||
const builder = new ShaderBuilder();
|
||||
|
||||
describe('check', function() {
|
||||
expect(builder.getSymbolFragmentShader(true)).to.eql(`precision mediump float;
|
||||
uniform float u_time;
|
||||
uniform float u_zoom;
|
||||
uniform float u_resolution;
|
||||
|
||||
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, variables, parseFn;
|
||||
|
||||
beforeEach(function() {
|
||||
attributes = [];
|
||||
variables = [];
|
||||
prefix = 'a_';
|
||||
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) - 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']);
|
||||
varying vec2 v_texCoord;
|
||||
varying vec2 v_quadCoord;
|
||||
varying vec4 v_hitColor;
|
||||
void main(void) {
|
||||
if (false) { discard; }
|
||||
gl_FragColor = vec4(1.0);
|
||||
gl_FragColor.rgb *= gl_FragColor.a;
|
||||
if (gl_FragColor.a < 0.1) { discard; } gl_FragColor = v_hitColor;
|
||||
}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -431,7 +253,7 @@ void main(void) {
|
||||
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.sizeExpression).to.eql('vec2(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);
|
||||
@@ -446,7 +268,7 @@ void main(void) {
|
||||
size: ['get', 'attr1'],
|
||||
color: [255, 127.5, 63.75, 0.25],
|
||||
textureCoord: [0.5, 0.5, 0.5, 1],
|
||||
offset: [3, ['get', 'attr3']]
|
||||
offset: ['match', ['get', 'attr3'], 'red', [6, 0], 'green', [3, 0], [0, 0]]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -460,8 +282,8 @@ void main(void) {
|
||||
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.sizeExpression).to.eql('vec2(a_attr1)');
|
||||
expect(result.builder.offsetExpression).to.eql('(a_attr3 == 1.0 ? vec2(6.0, 0.0) : (a_attr3 == 0.0 ? vec2(3.0, 0.0) : vec2(0.0, 0.0)))');
|
||||
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);
|
||||
@@ -487,7 +309,7 @@ void main(void) {
|
||||
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.sizeExpression).to.eql('vec2(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);
|
||||
@@ -503,7 +325,7 @@ void main(void) {
|
||||
},
|
||||
symbol: {
|
||||
symbolType: 'square',
|
||||
size: ['stretch', ['get', 'population'], ['var', 'lower'], ['var', 'higher'], 4, 8],
|
||||
size: ['interpolate', ['linear'], ['get', 'population'], ['var', 'lower'], 4, ['var', 'higher'], 8],
|
||||
color: '#336699',
|
||||
opacity: 0.5
|
||||
}
|
||||
@@ -520,7 +342,7 @@ void main(void) {
|
||||
'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))'
|
||||
'vec2(mix(4.0, 8.0, pow(clamp((a_population - u_lower) / (u_higher - u_lower), 0.0, 1.0), 1.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)');
|
||||
@@ -550,10 +372,10 @@ void main(void) {
|
||||
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.sizeExpression).to.eql('vec2(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.discardExpression).to.eql('!(v_attr0 >= 0.0 && v_attr0 <= 10.0)');
|
||||
expect(result.builder.rotateWithView).to.eql(false);
|
||||
expect(result.attributes.length).to.eql(1);
|
||||
expect(result.attributes[0].name).to.eql('attr0');
|
||||
@@ -564,16 +386,16 @@ void main(void) {
|
||||
symbol: {
|
||||
symbolType: 'square',
|
||||
size: 6,
|
||||
color: ['interpolate', ['var', 'ratio'], [255, 255, 0], 'red']
|
||||
color: ['interpolate', ['linear'], ['var', 'ratio'], 0, [255, 255, 0], 1, '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)'
|
||||
'vec4(mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), pow(clamp((u_ratio - 0.0) / (1.0 - 0.0), 0.0, 1.0), 1.0)).rgb, mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), pow(clamp((u_ratio - 0.0) / (1.0 - 0.0), 0.0, 1.0), 1.0)).a * 1.0 * 1.0)'
|
||||
);
|
||||
expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)');
|
||||
expect(result.builder.sizeExpression).to.eql('vec2(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);
|
||||
|
||||
Reference in New Issue
Block a user