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