ShaderBuilder / adapt logic & tests to new expressions module
This commit is contained in:
@@ -3,318 +3,7 @@
|
|||||||
* @module ol/webgl/ShaderBuilder
|
* @module ol/webgl/ShaderBuilder
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {asArray, isStringColor} from '../color.js';
|
import {expressionToGlsl, ValueTypes} from '../style/expressions.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Will return the number as a float with a dot separator, which is required by GLSL.
|
|
||||||
* @param {number} v Numerical value.
|
|
||||||
* @returns {string} The value as string.
|
|
||||||
*/
|
|
||||||
export function formatNumber(v) {
|
|
||||||
const s = v.toString();
|
|
||||||
return s.indexOf('.') === -1 ? s + '.0' : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will return the number array as a float with a dot separator, concatenated with ', '.
|
|
||||||
* @param {Array<number>} array Numerical values array.
|
|
||||||
* @returns {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
|
|
||||||
*/
|
|
||||||
export function formatArray(array) {
|
|
||||||
if (array.length < 2 || array.length > 4) {
|
|
||||||
throw new Error('`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.');
|
|
||||||
}
|
|
||||||
return `vec${array.length}(${array.map(formatNumber).join(', ')})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will normalize and converts to string a `vec4` color array compatible with GLSL.
|
|
||||||
* @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format,
|
|
||||||
* with RGB components in the 0..255 range and the alpha component in the 0..1 range.
|
|
||||||
* Note that the final array will always have 4 components.
|
|
||||||
* @returns {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form.
|
|
||||||
*/
|
|
||||||
export function formatColor(color) {
|
|
||||||
const array = asArray(color).slice();
|
|
||||||
if (array.length < 4) {
|
|
||||||
array.push(1);
|
|
||||||
}
|
|
||||||
return formatArray(
|
|
||||||
array.map(function(c, i) {
|
|
||||||
return i < 3 ? c / 255 : c;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible inferred types from a given value or expression
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
export const ValueTypes = {
|
|
||||||
UNKNOWN: -1,
|
|
||||||
NUMBER: 0,
|
|
||||||
STRING: 1,
|
|
||||||
COLOR: 2,
|
|
||||||
COLOR_OR_STRING: 3
|
|
||||||
};
|
|
||||||
|
|
||||||
function getValueType(value) {
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
return ValueTypes.NUMBER;
|
|
||||||
}
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
if (isStringColor(value)) {
|
|
||||||
return ValueTypes.COLOR_OR_STRING;
|
|
||||||
}
|
|
||||||
return ValueTypes.STRING;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
throw new Error(`Unrecognized value type: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
if (value.length === 3 || value.length === 4) {
|
|
||||||
const onlyNumbers = value.every(function(v) {
|
|
||||||
return typeof v === 'number';
|
|
||||||
});
|
|
||||||
if (onlyNumbers) {
|
|
||||||
return ValueTypes.COLOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof value[0] !== 'string') {
|
|
||||||
return ValueTypes.UNKNOWN;
|
|
||||||
}
|
|
||||||
switch (value[0]) {
|
|
||||||
case 'get':
|
|
||||||
case 'var':
|
|
||||||
case 'time':
|
|
||||||
case '*':
|
|
||||||
case '/':
|
|
||||||
case '+':
|
|
||||||
case '-':
|
|
||||||
case 'clamp':
|
|
||||||
case 'stretch':
|
|
||||||
case 'mod':
|
|
||||||
case 'pow':
|
|
||||||
case '>':
|
|
||||||
case '>=':
|
|
||||||
case '<':
|
|
||||||
case '<=':
|
|
||||||
case '==':
|
|
||||||
case '!':
|
|
||||||
case 'between':
|
|
||||||
return ValueTypes.NUMBER;
|
|
||||||
case 'interpolate':
|
|
||||||
return ValueTypes.COLOR;
|
|
||||||
default:
|
|
||||||
return ValueTypes.UNKNOWN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
|
|
||||||
* @returns {boolean} True if a numeric value, false otherwise
|
|
||||||
*/
|
|
||||||
export function isValueTypeNumber(value) {
|
|
||||||
return getValueType(value) === ValueTypes.NUMBER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
|
|
||||||
* @returns {boolean} True if a string value, false otherwise
|
|
||||||
*/
|
|
||||||
export function isValueTypeString(value) {
|
|
||||||
return getValueType(value) === ValueTypes.STRING || getValueType(value) === ValueTypes.COLOR_OR_STRING;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
|
|
||||||
* @returns {boolean} True if a color value, false otherwise
|
|
||||||
*/
|
|
||||||
export function isValueTypeColor(value) {
|
|
||||||
return getValueType(value) === ValueTypes.COLOR || getValueType(value) === ValueTypes.COLOR_OR_STRING;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that the provided value or expression is valid, and that the types used are compatible.
|
|
||||||
*
|
|
||||||
* Will throw an exception if found to be invalid.
|
|
||||||
*
|
|
||||||
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
|
|
||||||
*/
|
|
||||||
export function check(value) {
|
|
||||||
// these will be used to validate types in the expressions
|
|
||||||
function checkNumber(value) {
|
|
||||||
if (!isValueTypeNumber(value)) {
|
|
||||||
throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function checkColor(value) {
|
|
||||||
if (!isValueTypeColor(value)) {
|
|
||||||
throw new Error(`A color value was expected, got ${JSON.stringify(value)} instead`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function checkString(value) {
|
|
||||||
if (!isValueTypeString(value)) {
|
|
||||||
throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// first check that the value is of a recognized kind
|
|
||||||
if (!isValueTypeColor(value) && !isValueTypeNumber(value) && !isValueTypeString(value)) {
|
|
||||||
throw new Error(`No type could be inferred from the following expression: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check operator arguments
|
|
||||||
if (Array.isArray(value) && typeof value[0] === 'string') {
|
|
||||||
switch (value[0]) {
|
|
||||||
case 'get':
|
|
||||||
case 'var':
|
|
||||||
checkString(value[1]);
|
|
||||||
break;
|
|
||||||
case 'time':
|
|
||||||
break;
|
|
||||||
case '*':
|
|
||||||
case '/':
|
|
||||||
case '+':
|
|
||||||
case '-':
|
|
||||||
case 'mod':
|
|
||||||
case 'pow':
|
|
||||||
checkNumber(value[1]);
|
|
||||||
checkNumber(value[2]);
|
|
||||||
break;
|
|
||||||
case 'clamp':
|
|
||||||
checkNumber(value[1]);
|
|
||||||
checkNumber(value[2]);
|
|
||||||
checkNumber(value[3]);
|
|
||||||
break;
|
|
||||||
case 'stretch':
|
|
||||||
checkNumber(value[1]);
|
|
||||||
checkNumber(value[2]);
|
|
||||||
checkNumber(value[3]);
|
|
||||||
checkNumber(value[4]);
|
|
||||||
checkNumber(value[5]);
|
|
||||||
break;
|
|
||||||
case '>':
|
|
||||||
case '>=':
|
|
||||||
case '<':
|
|
||||||
case '<=':
|
|
||||||
case '==':
|
|
||||||
checkNumber(value[1]);
|
|
||||||
checkNumber(value[2]);
|
|
||||||
break;
|
|
||||||
case '!':
|
|
||||||
checkNumber(value[1]);
|
|
||||||
break;
|
|
||||||
case 'between':
|
|
||||||
checkNumber(value[1]);
|
|
||||||
checkNumber(value[2]);
|
|
||||||
checkNumber(value[3]);
|
|
||||||
break;
|
|
||||||
case 'interpolate':
|
|
||||||
checkNumber(value[1]);
|
|
||||||
checkColor(value[2]);
|
|
||||||
checkColor(value[3]);
|
|
||||||
break;
|
|
||||||
default: throw new Error(`Unrecognized operator in style expression: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the provided expressions and produces a GLSL-compatible assignment string, such as:
|
|
||||||
* `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)'
|
|
||||||
*
|
|
||||||
* Also takes in two arrays where new attributes and variables will be pushed, so that the user of the `parse` function
|
|
||||||
* knows which attributes/variables are expected to be available at evaluation time.
|
|
||||||
*
|
|
||||||
* For attributes, a prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in
|
|
||||||
* the final assignment string (depending on whether we're outputting a vertex or fragment shader).
|
|
||||||
*
|
|
||||||
* If a wrong value type is supplied to an operator (i. e. using colors with the `clamp` operator), an exception
|
|
||||||
* will be thrown.
|
|
||||||
*
|
|
||||||
* Note that by default, the `string` value type will be given precedence over `color`, so for example the
|
|
||||||
* `'yellow'` literal value will be parsed as a `string` while being a valid CSS color. This can be changed with
|
|
||||||
* the `typeHint` optional parameter which disambiguates what kind of value is expected.
|
|
||||||
*
|
|
||||||
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
|
|
||||||
* @param {Array<string>} attributes Array containing the attribute names **without a prefix**;
|
|
||||||
* it is passed along recursively
|
|
||||||
* @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`).
|
|
||||||
* @param {Array<string>} variables Array containing the variable names **without a prefix**;
|
|
||||||
* it is passed along recursively
|
|
||||||
* @param {ValueTypes} [typeHint] Hint for inferred type
|
|
||||||
* @returns {string} Assignment string.
|
|
||||||
*/
|
|
||||||
export function parse(value, attributes, attributePrefix, variables, typeHint) {
|
|
||||||
check(value);
|
|
||||||
|
|
||||||
function p(value) {
|
|
||||||
return parse(value, attributes, attributePrefix, variables);
|
|
||||||
}
|
|
||||||
function pC(value) {
|
|
||||||
return parse(value, attributes, attributePrefix, variables, ValueTypes.COLOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// operator
|
|
||||||
if (Array.isArray(value) && typeof value[0] === 'string') {
|
|
||||||
switch (value[0]) {
|
|
||||||
// reading operators
|
|
||||||
case 'get':
|
|
||||||
if (attributes.indexOf(value[1]) === -1) {
|
|
||||||
attributes.push(value[1]);
|
|
||||||
}
|
|
||||||
return attributePrefix + value[1];
|
|
||||||
case 'var':
|
|
||||||
if (variables.indexOf(value[1]) === -1) {
|
|
||||||
variables.push(value[1]);
|
|
||||||
}
|
|
||||||
return `u_${value[1]}`;
|
|
||||||
case 'time':
|
|
||||||
return 'u_time';
|
|
||||||
|
|
||||||
// math operators
|
|
||||||
case '*':
|
|
||||||
case '/':
|
|
||||||
case '+':
|
|
||||||
case '-':
|
|
||||||
return `(${p(value[1])} ${value[0]} ${p(value[2])})`;
|
|
||||||
case 'clamp': return `clamp(${p(value[1])}, ${p(value[2])}, ${p(value[3])})`;
|
|
||||||
case 'stretch':
|
|
||||||
const low1 = p(value[2]);
|
|
||||||
const high1 = p(value[3]);
|
|
||||||
const low2 = p(value[4]);
|
|
||||||
const high2 = p(value[5]);
|
|
||||||
return `((clamp(${p(value[1])}, ${low1}, ${high1}) - ${low1}) * ((${high2} - ${low2}) / (${high1} - ${low1})) + ${low2})`;
|
|
||||||
case 'mod': return `mod(${p(value[1])}, ${p(value[2])})`;
|
|
||||||
case 'pow': return `pow(${p(value[1])}, ${p(value[2])})`;
|
|
||||||
|
|
||||||
// color operators
|
|
||||||
case 'interpolate':
|
|
||||||
return `mix(${pC(value[2])}, ${pC(value[3])}, ${p(value[1])})`;
|
|
||||||
|
|
||||||
// logical operators
|
|
||||||
case '>':
|
|
||||||
case '>=':
|
|
||||||
case '<':
|
|
||||||
case '<=':
|
|
||||||
case '==':
|
|
||||||
return `(${p(value[1])} ${value[0]} ${p(value[2])} ? 1.0 : 0.0)`;
|
|
||||||
case '!':
|
|
||||||
return `(${p(value[1])} > 0.0 ? 0.0 : 1.0)`;
|
|
||||||
case 'between':
|
|
||||||
return `(${p(value[1])} >= ${p(value[2])} && ${p(value[1])} <= ${p(value[3])} ? 1.0 : 0.0)`;
|
|
||||||
|
|
||||||
default: throw new Error('Invalid style expression: ' + JSON.stringify(value));
|
|
||||||
}
|
|
||||||
} else if (isValueTypeNumber(value)) {
|
|
||||||
return formatNumber(/** @type {number} */(value));
|
|
||||||
} else if (isValueTypeString(value) && (typeHint === undefined || typeHint == ValueTypes.STRING)) {
|
|
||||||
return `"${value}"`;
|
|
||||||
} else {
|
|
||||||
return formatColor(/** @type {number[]|string} */(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} VaryingDescription
|
* @typedef {Object} VaryingDescription
|
||||||
@@ -660,21 +349,30 @@ export function parseLiteralStyle(style) {
|
|||||||
const offset = symbStyle.offset || [0, 0];
|
const offset = symbStyle.offset || [0, 0];
|
||||||
const opacity = symbStyle.opacity !== undefined ? symbStyle.opacity : 1;
|
const opacity = symbStyle.opacity !== undefined ? symbStyle.opacity : 1;
|
||||||
|
|
||||||
const variables = [];
|
const vertContext = {
|
||||||
const vertAttributes = [];
|
inFragmentShader: false,
|
||||||
// parse function for vertex shader
|
variables: [],
|
||||||
function pVert(value) {
|
attributes: []
|
||||||
return parse(value, vertAttributes, 'a_', variables);
|
};
|
||||||
}
|
const parseSizeX = expressionToGlsl(vertContext, size[0], ValueTypes.NUMBER);
|
||||||
|
const parseSizeY = expressionToGlsl(vertContext, size[1], ValueTypes.NUMBER);
|
||||||
|
const parsedOffsetX = expressionToGlsl(vertContext, offset[0], ValueTypes.NUMBER);
|
||||||
|
const parsedOffsetY = expressionToGlsl(vertContext, offset[1], ValueTypes.NUMBER);
|
||||||
|
const parsedTexCoordU1 = expressionToGlsl(vertContext, texCoord[0], ValueTypes.NUMBER);
|
||||||
|
const parsedTexCoordV1 = expressionToGlsl(vertContext, texCoord[1], ValueTypes.NUMBER);
|
||||||
|
const parsedTexCoordU2 = expressionToGlsl(vertContext, texCoord[2], ValueTypes.NUMBER);
|
||||||
|
const parsedTexCoordV2 = expressionToGlsl(vertContext, texCoord[3], ValueTypes.NUMBER);
|
||||||
|
|
||||||
const fragAttributes = [];
|
const fragContext = {
|
||||||
// parse function for fragment shader
|
inFragmentShader: true,
|
||||||
function pFrag(value, type) {
|
variables: vertContext.variables,
|
||||||
return parse(value, fragAttributes, 'v_', variables, type);
|
attributes: []
|
||||||
}
|
};
|
||||||
|
const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR);
|
||||||
|
const parsedOpacity = expressionToGlsl(fragContext, opacity, ValueTypes.NUMBER);
|
||||||
|
|
||||||
let opacityFilter = '1.0';
|
let opacityFilter = '1.0';
|
||||||
const visibleSize = pFrag(size[0]);
|
const visibleSize = expressionToGlsl(fragContext, size[0], ValueTypes.NUMBER);
|
||||||
switch (symbStyle.symbolType) {
|
switch (symbStyle.symbolType) {
|
||||||
case 'square': break;
|
case 'square': break;
|
||||||
case 'image': break;
|
case 'image': break;
|
||||||
@@ -691,26 +389,25 @@ export function parseLiteralStyle(style) {
|
|||||||
default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType);
|
default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedColor = pFrag(color, ValueTypes.COLOR);
|
|
||||||
|
|
||||||
const builder = new ShaderBuilder()
|
const builder = new ShaderBuilder()
|
||||||
.setSizeExpression(`vec2(${pVert(size[0])}, ${pVert(size[1])})`)
|
.setSizeExpression(`vec2(${parseSizeX}, ${parseSizeY})`)
|
||||||
.setSymbolOffsetExpression(`vec2(${pVert(offset[0])}, ${pVert(offset[1])})`)
|
.setSymbolOffsetExpression(`vec2(${parsedOffsetX}, ${parsedOffsetY})`)
|
||||||
.setTextureCoordinateExpression(
|
.setTextureCoordinateExpression(
|
||||||
`vec4(${pVert(texCoord[0])}, ${pVert(texCoord[1])}, ${pVert(texCoord[2])}, ${pVert(texCoord[3])})`)
|
`vec4(${parsedTexCoordU1}, ${parsedTexCoordV1}, ${parsedTexCoordU2}, ${parsedTexCoordV2})`)
|
||||||
.setSymbolRotateWithView(!!symbStyle.rotateWithView)
|
.setSymbolRotateWithView(!!symbStyle.rotateWithView)
|
||||||
.setColorExpression(
|
.setColorExpression(
|
||||||
`vec4(${parsedColor}.rgb, ${parsedColor}.a * ${pFrag(opacity)} * ${opacityFilter})`);
|
`vec4(${parsedColor}.rgb, ${parsedColor}.a * ${parsedOpacity} * ${opacityFilter})`);
|
||||||
|
|
||||||
if (style.filter) {
|
if (style.filter) {
|
||||||
builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`);
|
const parsedFilter = expressionToGlsl(fragContext, style.filter, ValueTypes.BOOLEAN);
|
||||||
|
builder.setFragmentDiscardExpression(`!${parsedFilter}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Object.<string,import("../webgl/Helper").UniformValue>} */
|
/** @type {Object.<string,import("../webgl/Helper").UniformValue>} */
|
||||||
const uniforms = {};
|
const uniforms = {};
|
||||||
|
|
||||||
// define one uniform per variable
|
// define one uniform per variable
|
||||||
variables.forEach(function(varName) {
|
fragContext.variables.forEach(function(varName) {
|
||||||
builder.addUniform(`float u_${varName}`);
|
builder.addUniform(`float u_${varName}`);
|
||||||
uniforms[`u_${varName}`] = function() {
|
uniforms[`u_${varName}`] = function() {
|
||||||
return style.variables && style.variables[varName] !== undefined ?
|
return style.variables && style.variables[varName] !== undefined ?
|
||||||
@@ -729,21 +426,21 @@ export function parseLiteralStyle(style) {
|
|||||||
|
|
||||||
// for each feature attribute used in the fragment shader, define a varying that will be used to pass data
|
// 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)
|
// from the vertex to the fragment shader, as well as an attribute in the vertex shader (if not already present)
|
||||||
fragAttributes.forEach(function(attrName) {
|
fragContext.attributes.forEach(function(attrName) {
|
||||||
if (vertAttributes.indexOf(attrName) === -1) {
|
if (vertContext.attributes.indexOf(attrName) === -1) {
|
||||||
vertAttributes.push(attrName);
|
vertContext.attributes.push(attrName);
|
||||||
}
|
}
|
||||||
builder.addVarying(`v_${attrName}`, 'float', `a_${attrName}`);
|
builder.addVarying(`v_${attrName}`, 'float', `a_${attrName}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// for each feature attribute used in the vertex shader, define an attribute in the vertex shader.
|
// for each feature attribute used in the vertex shader, define an attribute in the vertex shader.
|
||||||
vertAttributes.forEach(function(attrName) {
|
vertContext.attributes.forEach(function(attrName) {
|
||||||
builder.addAttribute(`float a_${attrName}`);
|
builder.addAttribute(`float a_${attrName}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
builder: builder,
|
builder: builder,
|
||||||
attributes: vertAttributes.map(function(attributeName) {
|
attributes: vertContext.attributes.map(function(attributeName) {
|
||||||
return {
|
return {
|
||||||
name: attributeName,
|
name: attributeName,
|
||||||
callback: function(feature) {
|
callback: function(feature) {
|
||||||
|
|||||||
@@ -1,112 +1,17 @@
|
|||||||
import {
|
import {parseLiteralStyle, ShaderBuilder} from '../../../../src/ol/webgl/ShaderBuilder.js';
|
||||||
check,
|
import {arrayToGlsl, colorToGlsl, numberToGlsl} from '../../../../src/ol/style/expressions.js';
|
||||||
formatArray,
|
|
||||||
formatColor,
|
|
||||||
formatNumber,
|
|
||||||
isValueTypeColor,
|
|
||||||
isValueTypeNumber,
|
|
||||||
isValueTypeString,
|
|
||||||
parse,
|
|
||||||
parseLiteralStyle,
|
|
||||||
ShaderBuilder,
|
|
||||||
ValueTypes
|
|
||||||
} from '../../../../src/ol/webgl/ShaderBuilder.js';
|
|
||||||
|
|
||||||
describe('ol.webgl.ShaderBuilder', function() {
|
describe('ol.webgl.ShaderBuilder', function() {
|
||||||
|
|
||||||
describe('formatNumber', function() {
|
|
||||||
it('does a simple transform when a fraction is present', function() {
|
|
||||||
expect(formatNumber(1.3456)).to.eql('1.3456');
|
|
||||||
});
|
|
||||||
it('adds a fraction separator when missing', function() {
|
|
||||||
expect(formatNumber(1)).to.eql('1.0');
|
|
||||||
expect(formatNumber(2.0)).to.eql('2.0');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatArray', function() {
|
|
||||||
it('outputs numbers with dot separators', function() {
|
|
||||||
expect(formatArray([1, 0, 3.45, 0.8888])).to.eql('vec4(1.0, 0.0, 3.45, 0.8888)');
|
|
||||||
expect(formatArray([3, 4])).to.eql('vec2(3.0, 4.0)');
|
|
||||||
});
|
|
||||||
it('throws on invalid lengths', function() {
|
|
||||||
let thrown = false;
|
|
||||||
try {
|
|
||||||
formatArray([3]);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
formatArray([3, 2, 1, 0, -1]);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
expect(thrown).to.be(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatColor', function() {
|
|
||||||
it('normalizes color and outputs numbers with dot separators', function() {
|
|
||||||
expect(formatColor([100, 0, 255])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
|
|
||||||
expect(formatColor([100, 0, 255, 1])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
|
|
||||||
});
|
|
||||||
it('handles colors in string format', function() {
|
|
||||||
expect(formatColor('red')).to.eql('vec4(1.0, 0.0, 0.0, 1.0)');
|
|
||||||
expect(formatColor('#00ff99')).to.eql('vec4(0.0, 1.0, 0.6, 1.0)');
|
|
||||||
expect(formatColor('rgb(100, 0, 255)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
|
|
||||||
expect(formatColor('rgba(100, 0, 255, 0.3)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 0.3)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('value type checking', function() {
|
|
||||||
it('correctly recognizes a number value', function() {
|
|
||||||
expect(isValueTypeNumber(1234)).to.eql(true);
|
|
||||||
expect(isValueTypeNumber(['time'])).to.eql(true);
|
|
||||||
expect(isValueTypeNumber(['clamp', ['get', 'attr'], -1, 1])).to.eql(true);
|
|
||||||
expect(isValueTypeNumber(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false);
|
|
||||||
expect(isValueTypeNumber('yellow')).to.eql(false);
|
|
||||||
expect(isValueTypeNumber('#113366')).to.eql(false);
|
|
||||||
expect(isValueTypeNumber('rgba(252,171,48,0.62)')).to.eql(false);
|
|
||||||
});
|
|
||||||
it('correctly recognizes a color value', function() {
|
|
||||||
expect(isValueTypeColor(1234)).to.eql(false);
|
|
||||||
expect(isValueTypeColor(['time'])).to.eql(false);
|
|
||||||
expect(isValueTypeColor(['clamp', ['get', 'attr'], -1, 1])).to.eql(false);
|
|
||||||
expect(isValueTypeColor(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(true);
|
|
||||||
expect(isValueTypeColor('yellow')).to.eql(true);
|
|
||||||
expect(isValueTypeColor('#113366')).to.eql(true);
|
|
||||||
expect(isValueTypeColor('rgba(252,171,48,0.62)')).to.eql(true);
|
|
||||||
expect(isValueTypeColor('abcd')).to.eql(false);
|
|
||||||
});
|
|
||||||
it('correctly recognizes a string value', function() {
|
|
||||||
expect(isValueTypeString(1234)).to.eql(false);
|
|
||||||
expect(isValueTypeString(['time'])).to.eql(false);
|
|
||||||
expect(isValueTypeString(['clamp', ['get', 'attr'], -1, 1])).to.eql(false);
|
|
||||||
expect(isValueTypeString(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false);
|
|
||||||
expect(isValueTypeString('yellow')).to.eql(true);
|
|
||||||
expect(isValueTypeString('#113366')).to.eql(true);
|
|
||||||
expect(isValueTypeString('rgba(252,171,48,0.62)')).to.eql(true);
|
|
||||||
expect(isValueTypeString('abcd')).to.eql(true);
|
|
||||||
});
|
|
||||||
it('throws on an unsupported type', function(done) {
|
|
||||||
try {
|
|
||||||
isValueTypeColor(true);
|
|
||||||
} catch (e) {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
done(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getSymbolVertexShader', function() {
|
describe('getSymbolVertexShader', function() {
|
||||||
it('generates a symbol vertex shader (with varying)', function() {
|
it('generates a symbol vertex shader (with varying)', function() {
|
||||||
const builder = new ShaderBuilder();
|
const builder = new ShaderBuilder();
|
||||||
builder.addVarying('v_opacity', 'float', formatNumber(0.4));
|
builder.addVarying('v_opacity', 'float', numberToGlsl(0.4));
|
||||||
builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3]));
|
builder.addVarying('v_test', 'vec3', arrayToGlsl([1, 2, 3]));
|
||||||
builder.setSizeExpression(`vec2(${formatNumber(6)})`);
|
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||||
builder.setSymbolOffsetExpression(formatArray([5, -7]));
|
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||||
builder.setColorExpression(formatColor([80, 0, 255, 1]));
|
builder.setColorExpression(colorToGlsl([80, 0, 255, 1]));
|
||||||
builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
|
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||||
|
|
||||||
expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
|
expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
|
||||||
uniform mat4 u_projectionMatrix;
|
uniform mat4 u_projectionMatrix;
|
||||||
@@ -144,10 +49,10 @@ void main(void) {
|
|||||||
const builder = new ShaderBuilder();
|
const builder = new ShaderBuilder();
|
||||||
builder.addUniform('float u_myUniform');
|
builder.addUniform('float u_myUniform');
|
||||||
builder.addAttribute('vec2 a_myAttr');
|
builder.addAttribute('vec2 a_myAttr');
|
||||||
builder.setSizeExpression(`vec2(${formatNumber(6)})`);
|
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||||
builder.setSymbolOffsetExpression(formatArray([5, -7]));
|
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||||
builder.setColorExpression(formatColor([80, 0, 255, 1]));
|
builder.setColorExpression(colorToGlsl([80, 0, 255, 1]));
|
||||||
builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
|
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||||
|
|
||||||
expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
|
expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
|
||||||
uniform mat4 u_projectionMatrix;
|
uniform mat4 u_projectionMatrix;
|
||||||
@@ -181,10 +86,10 @@ void main(void) {
|
|||||||
});
|
});
|
||||||
it('generates a symbol vertex shader (with rotateWithView)', function() {
|
it('generates a symbol vertex shader (with rotateWithView)', function() {
|
||||||
const builder = new ShaderBuilder();
|
const builder = new ShaderBuilder();
|
||||||
builder.setSizeExpression(`vec2(${formatNumber(6)})`);
|
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||||
builder.setSymbolOffsetExpression(formatArray([5, -7]));
|
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||||
builder.setColorExpression(formatColor([80, 0, 255, 1]));
|
builder.setColorExpression(colorToGlsl([80, 0, 255, 1]));
|
||||||
builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
|
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||||
builder.setSymbolRotateWithView(true);
|
builder.setSymbolRotateWithView(true);
|
||||||
|
|
||||||
expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
|
expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
|
||||||
@@ -222,12 +127,12 @@ 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 builder = new ShaderBuilder();
|
const builder = new ShaderBuilder();
|
||||||
builder.addVarying('v_opacity', 'float', formatNumber(0.4));
|
builder.addVarying('v_opacity', 'float', numberToGlsl(0.4));
|
||||||
builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3]));
|
builder.addVarying('v_test', 'vec3', arrayToGlsl([1, 2, 3]));
|
||||||
builder.setSizeExpression(`vec2(${formatNumber(6)})`);
|
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||||
builder.setSymbolOffsetExpression(formatArray([5, -7]));
|
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||||
builder.setColorExpression(formatColor([80, 0, 255]));
|
builder.setColorExpression(colorToGlsl([80, 0, 255]));
|
||||||
builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
|
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||||
|
|
||||||
expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
|
expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
|
||||||
uniform float u_time;
|
uniform float u_time;
|
||||||
@@ -246,10 +151,10 @@ void main(void) {
|
|||||||
const builder = new ShaderBuilder();
|
const builder = new ShaderBuilder();
|
||||||
builder.addUniform('float u_myUniform');
|
builder.addUniform('float u_myUniform');
|
||||||
builder.addUniform('vec2 u_myUniform2');
|
builder.addUniform('vec2 u_myUniform2');
|
||||||
builder.setSizeExpression(`vec2(${formatNumber(6)})`);
|
builder.setSizeExpression(`vec2(${numberToGlsl(6)})`);
|
||||||
builder.setSymbolOffsetExpression(formatArray([5, -7]));
|
builder.setSymbolOffsetExpression(arrayToGlsl([5, -7]));
|
||||||
builder.setColorExpression(formatColor([255, 255, 255, 1]));
|
builder.setColorExpression(colorToGlsl([255, 255, 255, 1]));
|
||||||
builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
|
builder.setTextureCoordinateExpression(arrayToGlsl([0, 0.5, 0.5, 1]));
|
||||||
builder.setFragmentDiscardExpression('u_myUniform > 0.5');
|
builder.setFragmentDiscardExpression('u_myUniform > 0.5');
|
||||||
|
|
||||||
expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
|
expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
|
||||||
@@ -267,154 +172,6 @@ void main(void) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('check', function() {
|
|
||||||
|
|
||||||
it('does not throw on valid expressions', function(done) {
|
|
||||||
check(1);
|
|
||||||
check('attr');
|
|
||||||
check('rgba(12, 34, 56, 0.5)');
|
|
||||||
check([255, 255, 255, 1]);
|
|
||||||
check([255, 255, 255]);
|
|
||||||
check(['get', 'myAttr']);
|
|
||||||
check(['var', 'myValue']);
|
|
||||||
check(['time']);
|
|
||||||
check(['+', ['*', ['get', 'size'], 0.001], 12]);
|
|
||||||
check(['/', ['-', ['get', 'size'], 0.001], 12]);
|
|
||||||
check(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20]);
|
|
||||||
check(['stretch', ['get', 'size'], 10, 100, 4, 8]);
|
|
||||||
check(['mod', ['pow', ['get', 'size'], 0.5], 12]);
|
|
||||||
check(['>', 10, ['get', 'attr4']]);
|
|
||||||
check(['>=', 10, ['get', 'attr4']]);
|
|
||||||
check(['<', 10, ['get', 'attr4']]);
|
|
||||||
check(['<=', 10, ['get', 'attr4']]);
|
|
||||||
check(['==', 10, ['get', 'attr4']]);
|
|
||||||
check(['between', ['get', 'attr4'], -4.0, 5.0]);
|
|
||||||
check(['!', ['get', 'attr4']]);
|
|
||||||
check(['interpolate', ['get', 'attr4'], 'green', '#3344FF']);
|
|
||||||
check(['interpolate', 0.2, [10, 20, 30], [255, 255, 255, 1]]);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws on unsupported types for operators', function() {
|
|
||||||
let thrown = false;
|
|
||||||
try {
|
|
||||||
check(['var', 1234]);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
check(['<', 0, 'aa']);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
check(['+', true, ['get', 'attr']]);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
check(['interpolate', ['get', 'attr4'], 1, '#3344FF']);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
expect(thrown).to.be(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws with the wrong number of arguments', function() {
|
|
||||||
let thrown = false;
|
|
||||||
try {
|
|
||||||
check(['var', 1234, 456]);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
check(['<', 4]);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
check(['+']);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
expect(thrown).to.be(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws on invalid expressions', function() {
|
|
||||||
let thrown = false;
|
|
||||||
try {
|
|
||||||
check(true);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
check([123, 456]);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
check(null);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
expect(thrown).to.be(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parse', function() {
|
|
||||||
let attributes, prefix, variables, parseFn;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
attributes = [];
|
|
||||||
variables = [];
|
|
||||||
prefix = 'a_';
|
|
||||||
parseFn = function(value, type) {
|
|
||||||
return parse(value, attributes, prefix, variables, type);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('parses expressions & literal values', function() {
|
|
||||||
expect(parseFn(1)).to.eql('1.0');
|
|
||||||
expect(parseFn('a_random_string')).to.eql('"a_random_string"');
|
|
||||||
expect(parseFn([255, 127.5, 63.75, 0.1])).to.eql('vec4(1.0, 0.5, 0.25, 0.1)');
|
|
||||||
expect(parseFn([255, 127.5, 63.75])).to.eql('vec4(1.0, 0.5, 0.25, 1.0)');
|
|
||||||
expect(parseFn(['get', 'myAttr'])).to.eql('a_myAttr');
|
|
||||||
expect(parseFn(['var', 'myValue'])).to.eql('u_myValue');
|
|
||||||
expect(parseFn(['time'])).to.eql('u_time');
|
|
||||||
expect(parseFn(['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)');
|
|
||||||
expect(parseFn(['/', ['-', ['get', 'size'], 20], 100])).to.eql('((a_size - 20.0) / 100.0)');
|
|
||||||
expect(parseFn(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)');
|
|
||||||
expect(parseFn(['stretch', ['get', 'size'], 10, 100, 4, 8])).to.eql('((clamp(a_size, 10.0, 100.0) - 10.0) * ((8.0 - 4.0) / (100.0 - 10.0)) + 4.0)');
|
|
||||||
expect(parseFn(['pow', ['mod', ['time'], 10], 2])).to.eql('pow(mod(u_time, 10.0), 2.0)');
|
|
||||||
expect(parseFn(['>', 10, ['get', 'attr4']])).to.eql('(10.0 > a_attr4 ? 1.0 : 0.0)');
|
|
||||||
expect(parseFn(['>=', 10, ['get', 'attr4']])).to.eql('(10.0 >= a_attr4 ? 1.0 : 0.0)');
|
|
||||||
expect(parseFn(['<', 10, ['get', 'attr4']])).to.eql('(10.0 < a_attr4 ? 1.0 : 0.0)');
|
|
||||||
expect(parseFn(['<=', 10, ['get', 'attr4']])).to.eql('(10.0 <= a_attr4 ? 1.0 : 0.0)');
|
|
||||||
expect(parseFn(['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4 ? 1.0 : 0.0)');
|
|
||||||
expect(parseFn(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0 ? 1.0 : 0.0)');
|
|
||||||
expect(parseFn(['!', ['get', 'attr4']])).to.eql('(a_attr4 > 0.0 ? 0.0 : 1.0)');
|
|
||||||
expect(parseFn(['interpolate', ['get', 'attr4'], [255, 255, 255, 1], 'transparent'])).to.eql(
|
|
||||||
'mix(vec4(1.0, 1.0, 1.0, 1.0), vec4(0.0, 0.0, 0.0, 0.0), a_attr4)');
|
|
||||||
expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3', 'attr4']);
|
|
||||||
expect(variables).to.eql(['myValue']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gives precedence to the string type unless asked otherwise', function() {
|
|
||||||
expect(parseFn('lightgreen')).to.eql('"lightgreen"');
|
|
||||||
expect(parseFn('lightgreen', ValueTypes.COLOR)).to.eql('vec4(0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1.0)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not register an attribute several times', function() {
|
|
||||||
parseFn(['get', 'myAttr']);
|
|
||||||
parseFn(['var', 'myVar']);
|
|
||||||
parseFn(['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]);
|
|
||||||
parseFn(['*', ['get', 'attr2'], ['var', 'myVar']]);
|
|
||||||
expect(attributes).to.eql(['myAttr', 'attr2']);
|
|
||||||
expect(variables).to.eql(['myVar']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseSymbolStyle', function() {
|
describe('parseSymbolStyle', function() {
|
||||||
it('parses a style without expressions', function() {
|
it('parses a style without expressions', function() {
|
||||||
const result = parseLiteralStyle({
|
const result = parseLiteralStyle({
|
||||||
@@ -553,7 +310,7 @@ void main(void) {
|
|||||||
expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.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.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.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)');
|
||||||
expect(result.builder.discardExpression).to.eql('(v_attr0 >= 0.0 && v_attr0 <= 10.0 ? 1.0 : 0.0) <= 0.0');
|
expect(result.builder.discardExpression).to.eql('(v_attr0 >= 0.0 && v_attr0 <= 10.0)');
|
||||||
expect(result.builder.rotateWithView).to.eql(false);
|
expect(result.builder.rotateWithView).to.eql(false);
|
||||||
expect(result.attributes.length).to.eql(1);
|
expect(result.attributes.length).to.eql(1);
|
||||||
expect(result.attributes[0].name).to.eql('attr0');
|
expect(result.attributes[0].name).to.eql('attr0');
|
||||||
|
|||||||
Reference in New Issue
Block a user