/** * Operators and utilities used for style expressions * @module ol/style/expressions */ import PaletteTexture from '../webgl/PaletteTexture.js'; import {Uniforms} from '../renderer/webgl/TileLayer.js'; import {asArray, fromString, isStringColor} from '../color.js'; /** * Base type used for literal style parameters; can be a number literal or the output of an operator, * which in turns takes {@link import("./expressions.js").ExpressionValue} arguments. * * The following operators can be used: * * * Reading operators: * * `['band', bandIndex, xOffset, yOffset]` For tile layers only. Fetches pixel values from band * `bandIndex` of the source's data. The first `bandIndex` of the source data is `1`. Fetched values * are in the 0..1 range. {@link import("../source/TileImage.js").default} sources have 4 bands: red, * green, blue and alpha. {@link import("../source/DataTile.js").default} sources can have any number * of bands, depending on the underlying data source and * {@link import("../source/GeoTIFF.js").Options configuration}. `xOffset` and `yOffset` are optional * and allow specifying pixel offsets for x and y. This is used for sampling data from neighboring pixels. * * `['get', 'attributeName', typeHint]` fetches a feature property value, similar to `feature.get('attributeName')` * A type hint can optionally be specified, in case the resulting expression contains a type ambiguity which * will make it invalid. Type hints can be one of: 'string', 'color', 'number', 'boolean', 'number[]' * * `['geometry-type']` returns a feature's geometry type as string, either: 'LineString', 'Point' or 'Polygon' * `Multi*` values are returned as their singular equivalent * `Circle` geometries are returned as 'Polygon' * `GeometryCollection` geometries are returned as the type of the first geometry found in the collection * * `['resolution']` returns the current resolution * * `['time']` returns the time in seconds since the creation of the layer * * `['var', 'varName']` fetches a value from the style variables; will throw an error if that variable is undefined * * `['zoom']` returns the current zoom level * * * Math operators: * * `['*', value1, value2, ...]` multiplies the values (either numbers or colors) * * `['/', value1, value2]` divides `value1` by `value2` * * `['+', value1, value2, ...]` adds the values * * `['-', value1, value2]` subtracts `value2` from `value1` * * `['clamp', value, low, high]` clamps `value` between `low` and `high` * * `['%', value1, value2]` returns the result of `value1 % value2` (modulo) * * `['^', value1, value2]` returns the value of `value1` raised to the `value2` power * * `['abs', value1]` returns the absolute value of `value1` * * `['floor', value1]` returns the nearest integer less than or equal to `value1` * * `['round', value1]` returns the nearest integer to `value1` * * `['ceil', value1]` returns the nearest integer greater than or equal to `value1` * * `['sin', value1]` returns the sine of `value1` * * `['cos', value1]` returns the cosine of `value1` * * `['atan', value1, value2]` returns `atan2(value1, value2)`. If `value2` is not provided, returns `atan(value1)` * * `['sqrt', value1]` returns the square root of `value1` * * * Transform operators: * * `['case', condition1, output1, ...conditionN, outputN, fallback]` selects the first output whose corresponding * condition evaluates to `true`. If no match is found, returns the `fallback` value. * All conditions should be `boolean`, output and fallback can be any kind. * * `['match', input, match1, output1, ...matchN, outputN, fallback]` compares the `input` value against all * provided `matchX` values, returning the output associated with the first valid match. If no match is found, * returns the `fallback` value. * `input` and `matchX` values must all be of the same type, and can be `number` or `string`. `outputX` and * `fallback` values must be of the same type, and can be of any kind. * * `['interpolate', interpolation, input, stop1, output1, ...stopN, outputN]` returns a value by interpolating between * pairs of inputs and outputs; `interpolation` can either be `['linear']` or `['exponential', base]` where `base` is * the rate of increase from stop A to stop B (i.e. power to which the interpolation ratio is raised); a value * of 1 is equivalent to `['linear']`. * `input` and `stopX` values must all be of type `number`. `outputX` values can be `number` or `color` values. * Note: `input` will be clamped between `stop1` and `stopN`, meaning that all output values will be comprised * between `output1` and `outputN`. * * * Logical operators: * * `['<', value1, value2]` returns `true` if `value1` is strictly lower than `value2`, or `false` otherwise. * * `['<=', value1, value2]` returns `true` if `value1` is lower than or equals `value2`, or `false` otherwise. * * `['>', value1, value2]` returns `true` if `value1` is strictly greater than `value2`, or `false` otherwise. * * `['>=', value1, value2]` returns `true` if `value1` is greater than or equals `value2`, or `false` otherwise. * * `['==', value1, value2]` returns `true` if `value1` equals `value2`, or `false` otherwise. * * `['!=', value1, value2]` returns `true` if `value1` does not equal `value2`, or `false` otherwise. * * `['!', value1]` returns `false` if `value1` is `true` or greater than `0`, or `true` otherwise. * * `['all', value1, value2, ...]` returns `true` if all the inputs are `true`, `false` otherwise. * * `['any', value1, value2, ...]` returns `true` if any of the inputs are `true`, `false` otherwise. * * `['between', value1, value2, value3]` returns `true` if `value1` is contained between `value2` and `value3` * (inclusively), or `false` otherwise. * * `['in', needle, haystack]` returns `true` if `needle` is found in `haystack`, and * `false` otherwise. * This operator has the following limitations: * * `haystack` has to be an array of numbers or strings (searching for a substring in a string is not supported yet) * * Only literal arrays are supported as `haystack` for now; this means that `haystack` cannot be the result of an * expression. If `haystack` is an array of strings, use the `literal` operator to disambiguate from an expression: * `['literal', ['abc', 'def', 'ghi']]` * * * Conversion operators: * * `['array', value1, ...valueN]` creates a numerical array from `number` values; please note that the amount of * values can currently only be 2, 3 or 4. * * `['color', red, green, blue, alpha]` creates a `color` value from `number` values; the `alpha` parameter is * optional; if not specified, it will be set to 1. * Note: `red`, `green` and `blue` components must be values between 0 and 255; `alpha` between 0 and 1. * * `['palette', index, colors]` picks a `color` value from an array of colors using the given index; the `index` * expression must evaluate to a number; the items in the `colors` array must be strings with hex colors * (e.g. `'#86A136'`), colors using the rgba[a] functional notation (e.g. `'rgb(134, 161, 54)'` or `'rgba(134, 161, 54, 1)'`), * named colors (e.g. `'red'`), or array literals with 3 ([r, g, b]) or 4 ([r, g, b, a]) values (with r, g, and b * in the 0-255 range and a in the 0-1 range). * * Values can either be literals or another operator, as they will be evaluated recursively. * Literal values can be of the following types: * * `boolean` * * `number` * * `number[]` (number arrays can only have a length of 2, 3 or 4) * * `string` * * {@link module:ol/color~Color} * * @typedef {Array<*>|import("../color.js").Color|string|number|boolean} ExpressionValue * @api */ /** * Possible inferred types from a given value or expression. * Note: these are binary flags. * @enum {number} */ export const ValueTypes = { NUMBER: 0b00001, STRING: 0b00010, COLOR: 0b00100, BOOLEAN: 0b01000, NUMBER_ARRAY: 0b10000, ANY: 0b11111, NONE: 0, }; /** * @param {string} typeHint Type hint * @return {ValueTypes} Resulting value type (will be a single type) */ function getTypeFromHint(typeHint) { switch (typeHint) { case 'string': return ValueTypes.STRING; case 'color': return ValueTypes.COLOR; case 'number': return ValueTypes.NUMBER; case 'boolean': return ValueTypes.BOOLEAN; case 'number[]': return ValueTypes.NUMBER_ARRAY; default: throw new Error(`Unrecognized type hint: ${typeHint}`); } } /** * An operator declaration must contain two methods: `getReturnType` which returns a type based on * the operator arguments, and `toGlsl` which returns a GLSL-compatible string. * Note: both methods can process arguments recursively. * @typedef {Object} Operator * @property {function(Array): ValueTypes|number} getReturnType Returns one or several types * @property {function(ParsingContext, Array, ValueTypes): string} toGlsl Returns a GLSL-compatible string * given a parsing context, an array of arguments and an expected type. * Note: the expected type can be a combination such as ValueTypes.NUMBER | ValueTypes.STRING or ValueTypes.ANY for instance */ /** * Operator declarations * @type {Object} */ export const Operators = {}; /** * Returns the possible types for a given value (each type being a binary flag) * To test a value use e.g. `getValueType(v) & ValueTypes.BOOLEAN` * @param {ExpressionValue} value Value * @return {ValueTypes|number} Type or types inferred from the value */ export function getValueType(value) { if (typeof value === 'number') { return ValueTypes.NUMBER; } if (typeof value === 'boolean') { return ValueTypes.BOOLEAN; } if (typeof value === 'string') { if (isStringColor(value)) { return ValueTypes.COLOR | ValueTypes.STRING; } return ValueTypes.STRING; } if (!Array.isArray(value)) { throw new Error(`Unhandled value type: ${JSON.stringify(value)}`); } const valueArr = /** @type {Array<*>} */ (value); const onlyNumbers = valueArr.every(function (v) { return typeof v === 'number'; }); if (onlyNumbers) { if (valueArr.length === 3 || valueArr.length === 4) { return ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY; } return ValueTypes.NUMBER_ARRAY; } if (typeof valueArr[0] !== 'string') { throw new Error( `Expected an expression operator but received: ${JSON.stringify( valueArr )}` ); } const operator = Operators[valueArr[0]]; if (operator === undefined) { throw new Error( `Unrecognized expression operator: ${JSON.stringify(valueArr)}` ); } return operator.getReturnType(valueArr.slice(1)); } /** * Checks if only one value type is enabled in the input number. * @param {ValueTypes|number} valueType Number containing value type binary flags * @return {boolean} True if only one type flag is enabled, false if zero or multiple */ export function isTypeUnique(valueType) { return Math.log2(valueType) % 1 === 0; } /** * Print types as a readable string * @param {ValueTypes|number} valueType Number containing value type binary flags * @return {string} Types */ function printTypes(valueType) { const result = []; if ((valueType & ValueTypes.NUMBER) > 0) { result.push('number'); } if ((valueType & ValueTypes.COLOR) > 0) { result.push('color'); } if ((valueType & ValueTypes.BOOLEAN) > 0) { result.push('boolean'); } if ((valueType & ValueTypes.NUMBER_ARRAY) > 0) { result.push('number[]'); } if ((valueType & ValueTypes.STRING) > 0) { result.push('string'); } return result.length > 0 ? result.join(', ') : '(no type)'; } /** * @typedef {Object} ParsingContextExternal * @property {string} name Name, unprefixed * @property {ValueTypes} type One of the value types constants * @property {function(import("../Feature.js").FeatureLike): *} [callback] Function used for computing the attribute value; * if undefined, `feature.get(attribute.name)` will be used */ /** * Context available during the parsing of an expression. * @typedef {Object} ParsingContext * @property {boolean} [inFragmentShader] If false, means the expression output should be made for a vertex shader * @property {Array} variables External variables used in the expression * @property {Array} attributes External attributes used in the expression * @property {Object} stringLiteralsMap This object maps all encountered string values to a number * @property {Object} functions Lookup of functions used by the style. * @property {number} [bandCount] Number of bands per pixel. * @property {Array} [paletteTextures] List of palettes used by the style. * @property {import("../style/literal").LiteralStyle} style The style being parsed */ /** * @param {string} operator Operator * @param {ParsingContext} context Parsing context * @return {string} A function name based on the operator, unique in the given context */ function computeOperatorFunctionName(operator, context) { return `operator_${operator}_${Object.keys(context.functions).length}`; } /** * Will return the number as a float with a dot separator, which is required by GLSL. * @param {number} v Numerical value. * @return {string} The value as string. */ export function numberToGlsl(v) { const s = v.toString(); return s.includes('.') ? s : s + '.0'; } /** * Will return the number array as a float with a dot separator, concatenated with ', '. * @param {Array} array Numerical values array. * @return {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`. */ export function arrayToGlsl(array) { if (array.length < 2 || array.length > 4) { throw new Error( '`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.' ); } return `vec${array.length}(${array.map(numberToGlsl).join(', ')})`; } /** * Will normalize and converts to string a `vec4` color array compatible with GLSL. * @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format, * with RGB components in the 0..255 range and the alpha component in the 0..1 range. * Note that the final array will always have 4 components. * @return {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form. */ export function colorToGlsl(color) { const array = asArray(color); const alpha = array.length > 3 ? array[3] : 1; // all components are premultiplied with alpha value return arrayToGlsl([ (array[0] / 255) * alpha, (array[1] / 255) * alpha, (array[2] / 255) * alpha, alpha, ]); } /** * Returns a stable equivalent number for the string literal. * @param {ParsingContext} context Parsing context * @param {string} string String literal value * @return {number} Number equivalent */ export function getStringNumberEquivalent(context, string) { if (context.stringLiteralsMap[string] === undefined) { context.stringLiteralsMap[string] = Object.keys( context.stringLiteralsMap ).length; } return context.stringLiteralsMap[string]; } /** * Returns a stable equivalent number for the string literal, for use in shaders. This number is then * converted to be a GLSL-compatible string. * @param {ParsingContext} context Parsing context * @param {string} string String literal value * @return {string} GLSL-compatible string containing a number */ export function stringToGlsl(context, string) { return numberToGlsl(getStringNumberEquivalent(context, string)); } /** * Recursively parses a style expression and outputs a GLSL-compatible string. Takes in a parsing context that * will be read and modified during the parsing operation. * @param {ParsingContext} context Parsing context * @param {ExpressionValue} value Value * @param {ValueTypes|number} [expectedType] Expected final type (can be several types combined) * If omitted, defaults to ValueTypes.NUMBER * @return {string} GLSL-compatible output */ export function expressionToGlsl(context, value, expectedType) { const returnType = expectedType !== undefined ? expectedType : ValueTypes.NUMBER; // operator if (Array.isArray(value) && typeof value[0] === 'string') { const operator = Operators[value[0]]; if (operator === undefined) { throw new Error( `Unrecognized expression operator: ${JSON.stringify(value)}` ); } return operator.toGlsl(context, value.slice(1), returnType); } const possibleType = getValueType(value) & returnType; assertNotEmptyType(value, possibleType, ''); if ((possibleType & ValueTypes.NUMBER) > 0) { return numberToGlsl(/** @type {number} */ (value)); } if ((possibleType & ValueTypes.BOOLEAN) > 0) { return value.toString(); } if ((possibleType & ValueTypes.STRING) > 0) { return stringToGlsl(context, value.toString()); } if ((possibleType & ValueTypes.COLOR) > 0) { return colorToGlsl(/** @type {Array | string} */ (value)); } if ((possibleType & ValueTypes.NUMBER_ARRAY) > 0) { return arrayToGlsl(/** @type {Array} */ (value)); } throw new Error( `Unexpected expression ${value} (expected type ${printTypes(returnType)})` ); } function assertNumber(value) { if ((getValueType(value) & ValueTypes.NUMBER) === 0) { throw new Error( `A numeric value was expected, got ${JSON.stringify(value)} instead` ); } } function assertNumbers(values) { for (let i = 0; i < values.length; i++) { assertNumber(values[i]); } } function assertString(value) { if ((getValueType(value) & ValueTypes.STRING) === 0) { throw new Error( `A string value was expected, got ${JSON.stringify(value)} instead` ); } } function assertBoolean(value) { if ((getValueType(value) & ValueTypes.BOOLEAN) === 0) { throw new Error( `A boolean value was expected, got ${JSON.stringify(value)} instead` ); } } function assertArgsCount(args, count) { if (args.length !== count) { throw new Error( `Exactly ${count} arguments were expected, got ${args.length} instead` ); } } function assertArgsMinCount(args, count) { if (args.length < count) { throw new Error( `At least ${count} arguments were expected, got ${args.length} instead` ); } } function assertArgsMaxCount(args, count) { if (args.length > count) { throw new Error( `At most ${count} arguments were expected, got ${args.length} instead` ); } } function assertArgsEven(args) { if (args.length % 2 !== 0) { throw new Error( `An even amount of arguments was expected, got ${JSON.stringify( args )} instead` ); } } function assertArgsOdd(args) { if (args.length % 2 === 0) { throw new Error( `An odd amount of arguments was expected, got ${JSON.stringify( args )} instead` ); } } function assertNotEmptyType(args, types, descriptor) { if (types === ValueTypes.NONE) { throw new Error( `No matching type was found for the following expression ${descriptor}: ${JSON.stringify( args )}` ); } } function assertSingleType(args, types, descriptor) { assertNotEmptyType(args, types, descriptor); if (!isTypeUnique(types)) { throw new Error( `Expected to have a unique type for the following expression ${descriptor}: ${JSON.stringify( args )} Got the following types instead: ${printTypes(types)}` ); } } function assertOfType(args, types, expectedTypes, descriptor) { if ((types & expectedTypes) === ValueTypes.NONE) { throw new Error( `Expected the ${descriptor} type of the following expression: ${JSON.stringify( args )} to be of the following types: ${printTypes(expectedTypes)} Got these types instead: ${printTypes(types)}` ); } } Operators['get'] = { getReturnType: function (args) { if (args.length === 2) { const hint = args[1]; return getTypeFromHint(/** @type {string} */ (hint)); } return ValueTypes.ANY; }, toGlsl: function (context, args, expectedType) { assertArgsMinCount(args, 1); assertArgsMaxCount(args, 2); assertString(args[0]); const outputType = expectedType & Operators['get'].getReturnType(args); assertSingleType(['get', ...args], outputType, ''); const name = args[0].toString(); const existing = context.attributes.find((a) => a.name === name); if (!existing) { context.attributes.push({ name: name, type: outputType, }); } else if (outputType !== existing.type) { throw new Error( `The following attribute was used in different places with incompatible types: ${name} Types were: ${printTypes(existing.type)} and ${printTypes(outputType)}` ); } const prefix = context.inFragmentShader ? 'v_' : 'a_'; return prefix + name; }, }; /** * Get the uniform name given a variable name. * @param {string} variableName The variable name. * @return {string} The uniform name. */ export function uniformNameForVariable(variableName) { return 'u_var_' + variableName; } Operators['var'] = { getReturnType: function () { return ValueTypes.ANY; }, toGlsl: function (context, args, expectedType) { assertArgsCount(args, 1); assertString(args[0]); const name = args[0].toString(); if ( !context.style.variables || context.style.variables[name] === undefined ) { throw new Error( `The following variable is missing from the style: ${name}` ); } const initialValue = context.style.variables[name]; const outputType = expectedType & getValueType(initialValue); assertSingleType(['var', ...args], outputType, ''); const existing = context.variables.find((a) => a.name === name); if (!existing) { context.variables.push({ name: name, type: outputType, }); } else if (outputType !== existing.type) { throw new Error( `The following variable was used in different places with incompatible types: ${name} Types were: ${printTypes(existing.type)} and ${printTypes(outputType)}` ); } return uniformNameForVariable(name); }, }; export const PALETTE_TEXTURE_ARRAY = 'u_paletteTextures'; // ['palette', index, colors] Operators['palette'] = { getReturnType: function () { return ValueTypes.COLOR; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumber(args[0]); const index = expressionToGlsl(context, args[0]); const colors = args[1]; if (!Array.isArray(colors)) { throw new Error('The second argument of palette must be an array'); } const numColors = colors.length; const palette = new Uint8Array(numColors * 4); for (let i = 0; i < numColors; i++) { const candidate = colors[i]; /** * @type {import('../color.js').Color} */ let color; if (typeof candidate === 'string') { color = fromString(candidate); } else { if (!Array.isArray(candidate)) { throw new Error( 'The second argument of palette must be an array of strings or colors' ); } const length = candidate.length; if (length === 4) { color = candidate; } else { if (length !== 3) { throw new Error( `Expected palette color to have 3 or 4 values, got ${length}` ); } color = [candidate[0], candidate[1], candidate[2], 1]; } } const offset = i * 4; palette[offset] = color[0]; palette[offset + 1] = color[1]; palette[offset + 2] = color[2]; palette[offset + 3] = color[3] * 255; } if (!context.paletteTextures) { context.paletteTextures = []; } const paletteName = `${PALETTE_TEXTURE_ARRAY}[${context.paletteTextures.length}]`; const paletteTexture = new PaletteTexture(paletteName, palette); context.paletteTextures.push(paletteTexture); return `texture2D(${paletteName}, vec2((${index} + 0.5) / ${numColors}.0, 0.5))`; }, }; const GET_BAND_VALUE_FUNC = 'getBandValue'; Operators['band'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsMinCount(args, 1); assertArgsMaxCount(args, 3); const band = args[0]; if (!(GET_BAND_VALUE_FUNC in context.functions)) { let ifBlocks = ''; const bandCount = context.bandCount || 1; for (let i = 0; i < bandCount; i++) { const colorIndex = Math.floor(i / 4); let bandIndex = i % 4; if (i === bandCount - 1 && bandIndex === 1) { // LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha bandIndex = 3; } const textureName = `${Uniforms.TILE_TEXTURE_ARRAY}[${colorIndex}]`; ifBlocks += ` if (band == ${i + 1}.0) { return texture2D(${textureName}, v_textureCoord + vec2(dx, dy))[${bandIndex}]; } `; } context.functions[GET_BAND_VALUE_FUNC] = ` float getBandValue(float band, float xOffset, float yOffset) { float dx = xOffset / ${Uniforms.TEXTURE_PIXEL_WIDTH}; float dy = yOffset / ${Uniforms.TEXTURE_PIXEL_HEIGHT}; ${ifBlocks} } `; } const bandExpression = expressionToGlsl(context, band); const xOffsetExpression = expressionToGlsl(context, args[1] || 0); const yOffsetExpression = expressionToGlsl(context, args[2] || 0); return `${GET_BAND_VALUE_FUNC}(${bandExpression}, ${xOffsetExpression}, ${yOffsetExpression})`; }, }; Operators['time'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 0); return 'u_time'; }, }; Operators['zoom'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 0); return 'u_zoom'; }, }; Operators['resolution'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 0); return 'u_resolution'; }, }; Operators['geometry-type'] = { getReturnType: function () { return ValueTypes.STRING; }, toGlsl: function (context, args) { assertArgsCount(args, 0); const name = 'geometryType'; const computeType = (geometry) => { const type = geometry.getType(); switch (type) { case 'Point': case 'LineString': case 'Polygon': return type; case 'MultiPoint': case 'MultiLineString': case 'MultiPolygon': return type.substring(5); case 'Circle': return 'Polygon'; case 'GeometryCollection': return computeType(geometry.getGeometries()[0]); default: } }; const existing = context.attributes.find((a) => a.name === name); if (!existing) { context.attributes.push({ name: name, type: ValueTypes.STRING, callback: (feature) => { return computeType(feature.getGeometry()); }, }); } const prefix = context.inFragmentShader ? 'v_' : 'a_'; return prefix + name; }, }; Operators['*'] = { getReturnType: function (args) { let outputType = ValueTypes.NUMBER | ValueTypes.COLOR; for (let i = 0; i < args.length; i++) { outputType = outputType & getValueType(args[i]); } return outputType; }, toGlsl: function (context, args, expectedType) { assertArgsMinCount(args, 2); let outputType = expectedType; for (let i = 0; i < args.length; i++) { outputType = outputType & getValueType(args[i]); } assertOfType( args, outputType, ValueTypes.NUMBER | ValueTypes.COLOR, 'output' ); return `(${args .map((arg) => expressionToGlsl(context, arg, outputType)) .join(' * ')})`; }, }; Operators['/'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `(${expressionToGlsl(context, args[0])} / ${expressionToGlsl( context, args[1] )})`; }, }; Operators['+'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsMinCount(args, 2); assertNumbers(args); return `(${args.map((arg) => expressionToGlsl(context, arg)).join(' + ')})`; }, }; Operators['-'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `(${expressionToGlsl(context, args[0])} - ${expressionToGlsl( context, args[1] )})`; }, }; Operators['clamp'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 3); assertNumbers(args); const min = expressionToGlsl(context, args[1]); const max = expressionToGlsl(context, args[2]); return `clamp(${expressionToGlsl(context, args[0])}, ${min}, ${max})`; }, }; Operators['%'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `mod(${expressionToGlsl(context, args[0])}, ${expressionToGlsl( context, args[1] )})`; }, }; Operators['^'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `pow(${expressionToGlsl(context, args[0])}, ${expressionToGlsl( context, args[1] )})`; }, }; Operators['abs'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertNumbers(args); return `abs(${expressionToGlsl(context, args[0])})`; }, }; Operators['floor'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertNumbers(args); return `floor(${expressionToGlsl(context, args[0])})`; }, }; Operators['round'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertNumbers(args); return `floor(${expressionToGlsl(context, args[0])} + 0.5)`; }, }; Operators['ceil'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertNumbers(args); return `ceil(${expressionToGlsl(context, args[0])})`; }, }; Operators['sin'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertNumbers(args); return `sin(${expressionToGlsl(context, args[0])})`; }, }; Operators['cos'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertNumbers(args); return `cos(${expressionToGlsl(context, args[0])})`; }, }; Operators['atan'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsMinCount(args, 1); assertArgsMaxCount(args, 2); assertNumbers(args); return args.length === 2 ? `atan(${expressionToGlsl(context, args[0])}, ${expressionToGlsl( context, args[1] )})` : `atan(${expressionToGlsl(context, args[0])})`; }, }; Operators['sqrt'] = { getReturnType: function () { return ValueTypes.NUMBER; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertNumbers(args); return `sqrt(${expressionToGlsl(context, args[0])})`; }, }; Operators['>'] = { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `(${expressionToGlsl(context, args[0])} > ${expressionToGlsl( context, args[1] )})`; }, }; Operators['>='] = { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `(${expressionToGlsl(context, args[0])} >= ${expressionToGlsl( context, args[1] )})`; }, }; Operators['<'] = { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `(${expressionToGlsl(context, args[0])} < ${expressionToGlsl( context, args[1] )})`; }, }; Operators['<='] = { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 2); assertNumbers(args); return `(${expressionToGlsl(context, args[0])} <= ${expressionToGlsl( context, args[1] )})`; }, }; function getEqualOperator(operator) { return { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 2); // find common type let type = ValueTypes.ANY; for (let i = 0; i < args.length; i++) { type &= getValueType(args[i]); } if (type === ValueTypes.NONE) { throw new Error( `All arguments should be of compatible type, got ${JSON.stringify( args )} instead` ); } // Since it's not possible to have color types here, we can leave it out // This fixes issues in case the value type is ambiguously detected as a color (e.g. the string 'red') type &= ~ValueTypes.COLOR; return `(${expressionToGlsl( context, args[0], type )} ${operator} ${expressionToGlsl(context, args[1], type)})`; }, }; } Operators['=='] = getEqualOperator('=='); Operators['!='] = getEqualOperator('!='); Operators['!'] = { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 1); assertBoolean(args[0]); return `(!${expressionToGlsl(context, args[0], ValueTypes.BOOLEAN)})`; }, }; function getDecisionOperator(operator) { return { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsMinCount(args, 2); for (let i = 0; i < args.length; i++) { assertBoolean(args[i]); } let result = args .map((arg) => expressionToGlsl(context, arg, ValueTypes.BOOLEAN)) .join(` ${operator} `); result = `(${result})`; return result; }, }; } Operators['all'] = getDecisionOperator('&&'); Operators['any'] = getDecisionOperator('||'); Operators['between'] = { getReturnType: function () { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 3); assertNumbers(args); const min = expressionToGlsl(context, args[1]); const max = expressionToGlsl(context, args[2]); const value = expressionToGlsl(context, args[0]); return `(${value} >= ${min} && ${value} <= ${max})`; }, }; Operators['array'] = { getReturnType: function () { return ValueTypes.NUMBER_ARRAY; }, toGlsl: function (context, args) { assertArgsMinCount(args, 2); assertArgsMaxCount(args, 4); assertNumbers(args); const parsedArgs = args.map(function (val) { return expressionToGlsl(context, val); }); return `vec${args.length}(${parsedArgs.join(', ')})`; }, }; Operators['color'] = { getReturnType: function () { return ValueTypes.COLOR; }, toGlsl: function (context, args) { assertArgsMinCount(args, 3); assertArgsMaxCount(args, 4); assertNumbers(args); const parsedArgs = args .slice(0, 3) .map((val) => `${expressionToGlsl(context, val)} / 255.0`); if (args.length === 3) { return `vec4(${parsedArgs.join(', ')}, 1.0)`; } const alpha = expressionToGlsl(context, args[3]); return `(${alpha} * vec4(${parsedArgs.join(', ')}, 1.0))`; }, }; Operators['interpolate'] = { getReturnType: function (args) { let type = ValueTypes.COLOR | ValueTypes.NUMBER; for (let i = 3; i < args.length; i += 2) { type = type & getValueType(args[i]); } return type; }, toGlsl: function (context, args, expectedType) { assertArgsEven(args); assertArgsMinCount(args, 6); // validate interpolation type const type = args[0]; let interpolation; switch (type[0]) { case 'linear': interpolation = 1; break; case 'exponential': interpolation = type[1]; break; default: interpolation = null; } if (!interpolation) { throw new Error( `Invalid interpolation type for "interpolate" operator, received: ${JSON.stringify( type )}` ); } // compute input/output types const inputType = ValueTypes.NUMBER; const outputType = Operators['interpolate'].getReturnType(args) & expectedType; assertSingleType(['interpolate', ...args], outputType, 'output'); const input = expressionToGlsl(context, args[1], inputType); const exponent = numberToGlsl(interpolation); let result = ''; for (let i = 2; i < args.length - 2; i += 2) { const stop1 = expressionToGlsl(context, args[i], inputType); const output1 = result || expressionToGlsl(context, args[i + 1], outputType); const stop2 = expressionToGlsl(context, args[i + 2], inputType); const output2 = expressionToGlsl(context, args[i + 3], outputType); let ratio; if (interpolation === 1) { ratio = `(${input} - ${stop1}) / (${stop2} - ${stop1})`; } else { ratio = `(pow(${exponent}, (${input} - ${stop1})) - 1.0) / (pow(${exponent}, (${stop2} - ${stop1})) - 1.0)`; } result = `mix(${output1}, ${output2}, clamp(${ratio}, 0.0, 1.0))`; } return result; }, }; Operators['match'] = { getReturnType: function (args) { let type = ValueTypes.ANY; for (let i = 2; i < args.length; i += 2) { type = type & getValueType(args[i]); } type = type & getValueType(args[args.length - 1]); return type; }, toGlsl: function (context, args, expectedType) { assertArgsEven(args); assertArgsMinCount(args, 4); let inputType = getValueType(args[0]); for (let i = 1; i < args.length - 1; i += 2) { inputType = inputType & getValueType(args[i]); } assertOfType( ['match', ...args], inputType, ValueTypes.STRING | ValueTypes.NUMBER | ValueTypes.BOOLEAN, 'input' ); inputType = (ValueTypes.STRING | ValueTypes.NUMBER | ValueTypes.BOOLEAN) & inputType; const outputType = Operators['match'].getReturnType(args) & expectedType; assertSingleType(['match', ...args], outputType, 'output'); const input = expressionToGlsl(context, args[0], inputType); const fallback = expressionToGlsl( context, args[args.length - 1], outputType ); let result = null; for (let i = args.length - 3; i >= 1; i -= 2) { const match = expressionToGlsl(context, args[i], inputType); const output = expressionToGlsl(context, args[i + 1], outputType); result = `(${input} == ${match} ? ${output} : ${result || fallback})`; } return result; }, }; Operators['case'] = { getReturnType: function (args) { let type = ValueTypes.ANY; for (let i = 1; i < args.length; i += 2) { type = type & getValueType(args[i]); } type = type & getValueType(args[args.length - 1]); return type; }, toGlsl: function (context, args, expectedType) { assertArgsOdd(args); assertArgsMinCount(args, 3); const outputType = Operators['case'].getReturnType(args) & expectedType; assertSingleType(['case', ...args], outputType, 'output'); for (let i = 0; i < args.length - 1; i += 2) { assertBoolean(args[i]); } const fallback = expressionToGlsl( context, args[args.length - 1], outputType ); let result = null; for (let i = args.length - 3; i >= 0; i -= 2) { const condition = expressionToGlsl(context, args[i], ValueTypes.BOOLEAN); const output = expressionToGlsl(context, args[i + 1], outputType); result = `(${condition} ? ${output} : ${result || fallback})`; } return result; }, }; Operators['in'] = { getReturnType: function (args) { return ValueTypes.BOOLEAN; }, toGlsl: function (context, args) { assertArgsCount(args, 2); const needle = args[0]; let haystack = args[1]; if (!Array.isArray(haystack)) { throw new Error( `The "in" operator expects an array literal as its second argument.` ); } if (typeof haystack[0] === 'string') { if (haystack[0] !== 'literal') { throw new Error( `For the "in" operator, a string array should be wrapped in a "literal" operator to disambiguate from expressions.` ); } if (!Array.isArray(haystack[1])) { throw new Error( `The "in" operator was provided a literal value which was not an array as second argument.` ); } haystack = haystack[1]; } let inputType = getValueType(needle); for (let i = 0; i < haystack.length - 1; i += 1) { inputType = inputType & getValueType(haystack[i]); } assertOfType( ['match', ...args], inputType, ValueTypes.STRING | ValueTypes.NUMBER | ValueTypes.BOOLEAN, 'input' ); inputType = (ValueTypes.STRING | ValueTypes.NUMBER | ValueTypes.BOOLEAN) & inputType; const funcName = computeOperatorFunctionName('in', context); const tests = []; for (let i = 0; i < haystack.length; i += 1) { tests.push( ` if (inputValue == ${expressionToGlsl( context, haystack[i], inputType )}) { return true; }` ); } context.functions[funcName] = `bool ${funcName}(float inputValue) { ${tests.join('\n')} return false; }`; return `${funcName}(${expressionToGlsl(context, needle, inputType)})`; }, };