UNPKG

ol

Version:

OpenLayers mapping library

863 lines (802 loc) • 30.7 kB
/** * Utilities for parsing flat styles for WebGL renderers * @module ol/render/webgl/style */ import {assert} from '../../asserts.js'; import { BooleanType, ColorType, NumberArrayType, NumberType, SizeType, StringType, computeGeometryType, } from '../../expr/expression.js'; import { FEATURE_ID_PROPERTY_NAME, GEOMETRY_TYPE_PROPERTY_NAME, getStringNumberEquivalent, newCompilationContext, stringToGlsl, } from '../../expr/gpu.js'; import {ShaderBuilder} from './ShaderBuilder.js'; import { applyContextToBuilder, expressionToGlsl, generateAttributesFromContext, generateUniformsFromContext, getGlslSizeFromType, getGlslTypeFromType, } from './compileUtil.js'; /** * see https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript * @param {Object|string} input The hash input, either an object or string * @return {string} Hash (if the object cannot be serialized, it is based on `getUid`) */ export function computeHash(input) { const hash = JSON.stringify(input) .split('') .reduce((prev, curr) => (prev << 5) - prev + curr.charCodeAt(0), 0); return (hash >>> 0).toString(); } /** * @param {import("../../style/flat.js").FlatStyle} style Style * @param {ShaderBuilder} builder Shader builder * @param {import("../../expr/gpu.js").CompilationContext} vertContext Vertex shader compilation context * @param {'shape-'|'circle-'|'icon-'} prefix Properties prefix */ function parseCommonSymbolProperties(style, builder, vertContext, prefix) { if (`${prefix}radius` in style && prefix !== 'icon-') { let radius = expressionToGlsl( vertContext, style[`${prefix}radius`], NumberType, ); if (`${prefix}radius2` in style) { const radius2 = expressionToGlsl( vertContext, style[`${prefix}radius2`], NumberType, ); radius = `max(${radius}, ${radius2})`; } if (`${prefix}stroke-width` in style) { radius = `(${radius} + ${expressionToGlsl( vertContext, style[`${prefix}stroke-width`], NumberType, )} * 0.5)`; } builder.setSymbolSizeExpression(`vec2(${radius} * 2. + 0.5)`); // adding some padding for antialiasing } if (`${prefix}scale` in style) { const scale = expressionToGlsl( vertContext, style[`${prefix}scale`], SizeType, ); builder.setSymbolSizeExpression( `${builder.getSymbolSizeExpression()} * ${scale}`, ); } if (`${prefix}displacement` in style) { builder.setSymbolOffsetExpression( expressionToGlsl( vertContext, style[`${prefix}displacement`], NumberArrayType, ), ); } if (`${prefix}rotation` in style) { builder.setSymbolRotationExpression( expressionToGlsl(vertContext, style[`${prefix}rotation`], NumberType), ); } if (`${prefix}rotate-with-view` in style) { builder.setSymbolRotateWithView(!!style[`${prefix}rotate-with-view`]); } } /** * @param {string} distanceField The distance field expression * @param {string|null} fillColor The fill color expression; null if no fill * @param {string|null} strokeColor The stroke color expression; null if no stroke * @param {string|null} strokeWidth The stroke width expression; null if no stroke * @param {string|null} opacity The opacity expression; null if no stroke * @return {string} The final color expression, based on the distance field and given params */ function getColorFromDistanceField( distanceField, fillColor, strokeColor, strokeWidth, opacity, ) { let color = 'vec4(0.)'; if (fillColor !== null) { color = fillColor; } if (strokeColor !== null && strokeWidth !== null) { const strokeFillRatio = `smoothstep(-${strokeWidth} + 0.63, -${strokeWidth} - 0.58, ${distanceField})`; color = `mix(${strokeColor}, ${color}, ${strokeFillRatio})`; } const shapeOpacity = `(1.0 - smoothstep(-0.63, 0.58, ${distanceField}))`; let result = `${color} * vec4(1.0, 1.0, 1.0, ${shapeOpacity})`; if (opacity !== null) { result = `${result} * vec4(1.0, 1.0, 1.0, ${opacity})`; } return result; } /** * This will parse an image property provided by `<prefix>-src` * The image size expression in GLSL will be returned * @param {import("../../style/flat.js").FlatStyle} style Style * @param {ShaderBuilder} builder Shader builder * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms * @param {'icon-'|'fill-pattern-'|'stroke-pattern-'} prefix Property prefix * @param {string} textureId A identifier that will be used in the generated uniforms: `sample2d u_texture<id>` and `vec2 u_texture<id>_size` * @return {string} The image size expression */ function parseImageProperties(style, builder, uniforms, prefix, textureId) { const image = new Image(); image.crossOrigin = style[`${prefix}cross-origin`] === undefined ? 'anonymous' : style[`${prefix}cross-origin`]; assert( typeof style[`${prefix}src`] === 'string', `WebGL layers do not support expressions for the ${prefix}src style property`, ); image.src = /** @type {string} */ (style[`${prefix}src`]); // the size is provided asynchronously using a uniform uniforms[`u_texture${textureId}_size`] = () => { return image.complete ? [image.width, image.height] : [0, 0]; }; builder.addUniform(`u_texture${textureId}_size`, 'vec2'); const size = `u_texture${textureId}_size`; uniforms[`u_texture${textureId}`] = image; builder.addUniform(`u_texture${textureId}`, 'sampler2D'); return size; } /** * This will parse an image's offset properties provided by `<prefix>-offset`, `<prefix>-offset-origin` and `<prefix>-size` * @param {import("../../style/flat.js").FlatStyle} style Style * @param {'icon-'|'fill-pattern-'|'stroke-pattern-'} prefix Property prefix * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context (vertex or fragment) * @param {string} imageSize Pixel size of the full image as a GLSL expression * @param {string} sampleSize Pixel size of the sample in the image as a GLSL expression * @return {string} The offset expression */ function parseImageOffsetProperties( style, prefix, context, imageSize, sampleSize, ) { let offsetExpression = expressionToGlsl( context, style[`${prefix}offset`], SizeType, ); if (`${prefix}offset-origin` in style) { switch (style[`${prefix}offset-origin`]) { case 'top-right': offsetExpression = `vec2(${imageSize}.x, 0.) + ${sampleSize} * vec2(-1., 0.) + ${offsetExpression} * vec2(-1., 1.)`; break; case 'bottom-left': offsetExpression = `vec2(0., ${imageSize}.y) + ${sampleSize} * vec2(0., -1.) + ${offsetExpression} * vec2(1., -1.)`; break; case 'bottom-right': offsetExpression = `${imageSize} - ${sampleSize} - ${offsetExpression}`; break; default: // pass } } return offsetExpression; } /** * @param {import("../../style/flat.js").FlatStyle} style Style * @param {ShaderBuilder} builder Shader builder * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context */ function parseCircleProperties(style, builder, uniforms, context) { // this function takes in screen coordinates in pixels and returns the signed distance field // (0 on the boundary, negative inside the circle, positive outside, values in pixels) context.functions['circleDistanceField'] = `float circleDistanceField(vec2 point, float radius) { return length(point) - radius; }`; parseCommonSymbolProperties(style, builder, context, 'circle-'); // OPACITY let opacity = null; if ('circle-opacity' in style) { opacity = expressionToGlsl(context, style['circle-opacity'], NumberType); } // SCALE let currentPoint = 'coordsPx'; if ('circle-scale' in style) { const scale = expressionToGlsl(context, style['circle-scale'], SizeType); currentPoint = `coordsPx / ${scale}`; } // FILL COLOR let fillColor = null; if ('circle-fill-color' in style) { fillColor = expressionToGlsl( context, style['circle-fill-color'], ColorType, ); } // STROKE COLOR let strokeColor = null; if ('circle-stroke-color' in style) { strokeColor = expressionToGlsl( context, style['circle-stroke-color'], ColorType, ); } // RADIUS let radius = expressionToGlsl(context, style['circle-radius'], NumberType); // STROKE WIDTH let strokeWidth = null; if ('circle-stroke-width' in style) { strokeWidth = expressionToGlsl( context, style['circle-stroke-width'], NumberType, ); radius = `(${radius} + ${strokeWidth} * 0.5)`; } // FINAL COLOR const distanceField = `circleDistanceField(${currentPoint}, ${radius})`; const colorExpression = getColorFromDistanceField( distanceField, fillColor, strokeColor, strokeWidth, opacity, ); builder.setSymbolColorExpression(colorExpression); } /** * @param {import("../../style/flat.js").FlatStyle} style Style * @param {ShaderBuilder} builder Shader builder * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context */ function parseShapeProperties(style, builder, uniforms, context) { context.functions['round'] = `float round(float v) { return sign(v) * floor(abs(v) + 0.5); }`; // these functions take in screen coordinates in pixels and returns the signed distance field // (0 on the boundary, negative inside the polygon, positive outside, values in pixels) // inspired by https://github.com/zranger1/PixelblazePatterns/blob/master/Toolkit/sdf2d.md#n-sided-regular-polygon context.functions['starDistanceField'] = `float starDistanceField(vec2 point, float numPoints, float radius, float radius2, float angle) { float startAngle = -PI * 0.5 + angle; // tip starts upwards and rotates clockwise with angle float c = cos(startAngle); float s = sin(startAngle); vec2 pointRotated = vec2(c * point.x - s * point.y, s * point.x + c * point.y); float alpha = TWO_PI / numPoints; // the angle of one sector float beta = atan(pointRotated.y, pointRotated.x); float gamma = round(beta / alpha) * alpha; // angle in sector c = cos(-gamma); s = sin(-gamma); vec2 inSector = vec2(c * pointRotated.x - s * pointRotated.y, abs(s * pointRotated.x + c * pointRotated.y)); vec2 tipToPoint = inSector + vec2(-radius, 0.); vec2 edgeNormal = vec2(radius2 * sin(alpha * 0.5), -radius2 * cos(alpha * 0.5) + radius); return dot(normalize(edgeNormal), tipToPoint); }`; context.functions['regularDistanceField'] = `float regularDistanceField(vec2 point, float numPoints, float radius, float angle) { float startAngle = -PI * 0.5 + angle; // tip starts upwards and rotates clockwise with angle float c = cos(startAngle); float s = sin(startAngle); vec2 pointRotated = vec2(c * point.x - s * point.y, s * point.x + c * point.y); float alpha = TWO_PI / numPoints; // the angle of one sector float radiusIn = radius * cos(PI / numPoints); float beta = atan(pointRotated.y, pointRotated.x); float gamma = round((beta - alpha * 0.5) / alpha) * alpha + alpha * 0.5; // angle in sector from mid c = cos(-gamma); s = sin(-gamma); vec2 inSector = vec2(c * pointRotated.x - s * pointRotated.y, abs(s * pointRotated.x + c * pointRotated.y)); return inSector.x - radiusIn; }`; parseCommonSymbolProperties(style, builder, context, 'shape-'); // OPACITY let opacity = null; if ('shape-opacity' in style) { opacity = expressionToGlsl(context, style['shape-opacity'], NumberType); } // SCALE let currentPoint = 'coordsPx'; if ('shape-scale' in style) { const scale = expressionToGlsl(context, style['shape-scale'], SizeType); currentPoint = `coordsPx / ${scale}`; } // FILL COLOR let fillColor = null; if ('shape-fill-color' in style) { fillColor = expressionToGlsl(context, style['shape-fill-color'], ColorType); } // STROKE COLOR let strokeColor = null; if ('shape-stroke-color' in style) { strokeColor = expressionToGlsl( context, style['shape-stroke-color'], ColorType, ); } // STROKE WIDTH let strokeWidth = null; if ('shape-stroke-width' in style) { strokeWidth = expressionToGlsl( context, style['shape-stroke-width'], NumberType, ); } // SHAPE TYPE const numPoints = expressionToGlsl( context, style['shape-points'], NumberType, ); let angle = '0.'; if ('shape-angle' in style) { angle = expressionToGlsl(context, style['shape-angle'], NumberType); } let shapeField; let radius = expressionToGlsl(context, style['shape-radius'], NumberType); if (strokeWidth !== null) { radius = `${radius} + ${strokeWidth} * 0.5`; } if ('shape-radius2' in style) { let radius2 = expressionToGlsl(context, style['shape-radius2'], NumberType); if (strokeWidth !== null) { radius2 = `${radius2} + ${strokeWidth} * 0.5`; } shapeField = `starDistanceField(${currentPoint}, ${numPoints}, ${radius}, ${radius2}, ${angle})`; } else { shapeField = `regularDistanceField(${currentPoint}, ${numPoints}, ${radius}, ${angle})`; } // FINAL COLOR const colorExpression = getColorFromDistanceField( shapeField, fillColor, strokeColor, strokeWidth, opacity, ); builder.setSymbolColorExpression(colorExpression); } /** * @param {import("../../style/flat.js").FlatStyle} style Style * @param {ShaderBuilder} builder Shader builder * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context */ function parseIconProperties(style, builder, uniforms, context) { // COLOR let color = 'vec4(1.0)'; if ('icon-color' in style) { color = expressionToGlsl(context, style['icon-color'], ColorType); } // OPACITY if ('icon-opacity' in style) { color = `${color} * vec4(1.0, 1.0, 1.0, ${expressionToGlsl( context, style['icon-opacity'], NumberType, )})`; } // IMAGE & SIZE const textureId = computeHash(style['icon-src']); const sizeExpression = parseImageProperties( style, builder, uniforms, 'icon-', textureId, ); builder .setSymbolColorExpression( `${color} * texture2D(u_texture${textureId}, v_texCoord)`, ) .setSymbolSizeExpression(sizeExpression); // override size if width/height were specified if ('icon-width' in style && 'icon-height' in style) { builder.setSymbolSizeExpression( `vec2(${expressionToGlsl( context, style['icon-width'], NumberType, )}, ${expressionToGlsl(context, style['icon-height'], NumberType)})`, ); } // tex coord if ('icon-offset' in style && 'icon-size' in style) { const sampleSize = expressionToGlsl( context, style['icon-size'], NumberArrayType, ); const fullsize = builder.getSymbolSizeExpression(); builder.setSymbolSizeExpression(sampleSize); const offset = parseImageOffsetProperties( style, 'icon-', context, 'v_quadSizePx', sampleSize, ); builder.setTextureCoordinateExpression( `(vec4((${offset}).xyxy) + vec4(0., 0., ${sampleSize})) / (${fullsize}).xyxy`, ); } parseCommonSymbolProperties(style, builder, context, 'icon-'); if ('icon-anchor' in style) { const anchor = expressionToGlsl( context, style['icon-anchor'], NumberArrayType, ); let scale = `1.0`; if (`icon-scale` in style) { scale = expressionToGlsl(context, style[`icon-scale`], SizeType); } let shiftPx; if ( style['icon-anchor-x-units'] === 'pixels' && style['icon-anchor-y-units'] === 'pixels' ) { shiftPx = `${anchor} * ${scale}`; } else if (style['icon-anchor-x-units'] === 'pixels') { shiftPx = `${anchor} * vec2(vec2(${scale}).x, v_quadSizePx.y)`; } else if (style['icon-anchor-y-units'] === 'pixels') { shiftPx = `${anchor} * vec2(v_quadSizePx.x, vec2(${scale}).x)`; } else { shiftPx = `${anchor} * v_quadSizePx`; } // default origin is top-left let offsetPx = `v_quadSizePx * vec2(0.5, -0.5) + ${shiftPx} * vec2(-1., 1.)`; if ('icon-anchor-origin' in style) { switch (style['icon-anchor-origin']) { case 'top-right': offsetPx = `v_quadSizePx * -0.5 + ${shiftPx}`; break; case 'bottom-left': offsetPx = `v_quadSizePx * 0.5 - ${shiftPx}`; break; case 'bottom-right': offsetPx = `v_quadSizePx * vec2(-0.5, 0.5) + ${shiftPx} * vec2(1., -1.)`; break; default: // pass } } builder.setSymbolOffsetExpression( `${builder.getSymbolOffsetExpression()} + ${offsetPx}`, ); } } /** * @param {import("../../style/flat.js").FlatStyle} style Style * @param {ShaderBuilder} builder Shader Builder * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context */ function parseStrokeProperties(style, builder, uniforms, context) { if ('stroke-color' in style) { builder.setStrokeColorExpression( expressionToGlsl(context, style['stroke-color'], ColorType), ); } if ('stroke-pattern-src' in style) { const textureId = computeHash(style['stroke-pattern-src']); const sizeExpression = parseImageProperties( style, builder, uniforms, 'stroke-pattern-', textureId, ); let sampleSizeExpression = sizeExpression; let offsetExpression = 'vec2(0.)'; if ('stroke-pattern-offset' in style && 'stroke-pattern-size' in style) { sampleSizeExpression = expressionToGlsl( context, style[`stroke-pattern-size`], NumberArrayType, ); offsetExpression = parseImageOffsetProperties( style, 'stroke-pattern-', context, sizeExpression, sampleSizeExpression, ); } let spacingExpression = '0.'; if ('stroke-pattern-spacing' in style) { spacingExpression = expressionToGlsl( context, style['stroke-pattern-spacing'], NumberType, ); } let startOffsetExpression = '0.'; if ('stroke-pattern-start-offset' in style) { startOffsetExpression = expressionToGlsl( context, style['stroke-pattern-start-offset'], NumberType, ); } context.functions['sampleStrokePattern'] = `vec4 sampleStrokePattern(sampler2D texture, vec2 textureSize, vec2 textureOffset, vec2 sampleSize, float spacingPx, float startOffsetPx, float currentLengthPx, float currentRadiusRatio, float lineWidth) { float currentLengthScaled = (currentLengthPx - startOffsetPx) * sampleSize.y / lineWidth; float spacingScaled = spacingPx * sampleSize.y / lineWidth; float uCoordPx = mod(currentLengthScaled, (sampleSize.x + spacingScaled)); float isInsideOfPattern = step(uCoordPx, sampleSize.x); float vCoordPx = (-currentRadiusRatio * 0.5 + 0.5) * sampleSize.y; // make sure that we're not sampling too close to the borders to avoid interpolation with outside pixels uCoordPx = clamp(uCoordPx, 0.5, sampleSize.x - 0.5); vCoordPx = clamp(vCoordPx, 0.5, sampleSize.y - 0.5); vec2 texCoord = (vec2(uCoordPx, vCoordPx) + textureOffset) / textureSize; return texture2D(texture, texCoord) * vec4(1.0, 1.0, 1.0, isInsideOfPattern); }`; const textureName = `u_texture${textureId}`; let tintExpression = '1.'; if ('stroke-color' in style) { tintExpression = builder.getStrokeColorExpression(); } builder.setStrokeColorExpression( `${tintExpression} * sampleStrokePattern(${textureName}, ${sizeExpression}, ${offsetExpression}, ${sampleSizeExpression}, ${spacingExpression}, ${startOffsetExpression}, currentLengthPx, currentRadiusRatio, v_width)`, ); context.functions['computeStrokePatternLength'] = `float computeStrokePatternLength(vec2 sampleSize, float spacingPx, float lineWidth) { float patternLengthPx = sampleSize.x / sampleSize.y * lineWidth; return patternLengthPx + spacingPx; }`; // apply a stroke pattern length to avoid visual artifacts builder.setStrokePatternLengthExpression( `computeStrokePatternLength(${sampleSizeExpression}, ${spacingExpression}, v_width)`, ); } if ('stroke-width' in style) { builder.setStrokeWidthExpression( expressionToGlsl(context, style['stroke-width'], NumberType), ); } if ('stroke-offset' in style) { builder.setStrokeOffsetExpression( expressionToGlsl(context, style['stroke-offset'], NumberType), ); } if ('stroke-line-cap' in style) { builder.setStrokeCapExpression( expressionToGlsl(context, style['stroke-line-cap'], StringType), ); } if ('stroke-line-join' in style) { builder.setStrokeJoinExpression( expressionToGlsl(context, style['stroke-line-join'], StringType), ); } if ('stroke-miter-limit' in style) { builder.setStrokeMiterLimitExpression( expressionToGlsl(context, style['stroke-miter-limit'], NumberType), ); } if ('stroke-line-dash' in style) { context.functions['getSingleDashDistance'] = `float getSingleDashDistance(float distance, float radius, float dashOffset, float dashLength, float dashLengthTotal, float capType, float lineWidth) { float localDistance = mod(distance, dashLengthTotal); float distanceSegment = abs(localDistance - dashOffset - dashLength * 0.5) - dashLength * 0.5; distanceSegment = min(distanceSegment, dashLengthTotal - localDistance); if (capType == ${stringToGlsl('square')}) { distanceSegment -= lineWidth * 0.5; } else if (capType == ${stringToGlsl('round')}) { distanceSegment = min(distanceSegment, sqrt(distanceSegment * distanceSegment + radius * radius) - lineWidth * 0.5); } return distanceSegment; }`; let dashPattern = style['stroke-line-dash'].map((v) => expressionToGlsl(context, v, NumberType), ); // if pattern has odd length, concatenate it with itself to be even if (dashPattern.length % 2 === 1) { dashPattern = [...dashPattern, ...dashPattern]; } let offsetExpression = '0.'; if ('stroke-line-dash-offset' in style) { offsetExpression = expressionToGlsl( context, style['stroke-line-dash-offset'], NumberType, ); } // define a function for this dash specifically const uniqueDashKey = computeHash(style['stroke-line-dash']); const dashFunctionName = `dashDistanceField_${uniqueDashKey}`; const dashLengthsParamsDef = dashPattern .map((v, i) => `float dashLength${i}`) .join(', '); const totalLengthDef = dashPattern .map((v, i) => `dashLength${i}`) .join(' + '); let currentDashOffset = '0.'; let distanceExpression = `getSingleDashDistance(distance, radius, ${currentDashOffset}, dashLength0, totalDashLength, capType, lineWidth)`; for (let i = 2; i < dashPattern.length; i += 2) { currentDashOffset = `${currentDashOffset} + dashLength${ i - 2 } + dashLength${i - 1}`; distanceExpression = `min(${distanceExpression}, getSingleDashDistance(distance, radius, ${currentDashOffset}, dashLength${i}, totalDashLength, capType, lineWidth))`; } context.functions[dashFunctionName] = `float ${dashFunctionName}(float distance, float radius, float capType, float lineWidth, ${dashLengthsParamsDef}) { float totalDashLength = ${totalLengthDef}; return ${distanceExpression}; }`; const dashLengthsCalls = dashPattern.map((v, i) => `${v}`).join(', '); builder.setStrokeDistanceFieldExpression( `${dashFunctionName}(currentLengthPx + ${offsetExpression}, currentRadiusPx, capType, v_width, ${dashLengthsCalls})`, ); // apply a stroke pattern length to avoid visual artifacts let patternLength = dashPattern.join(' + '); if (builder.getStrokePatternLengthExpression()) { context.functions['combinePatternLengths'] = `float combinePatternLengths(float patternLength1, float patternLength2) { return patternLength1 * patternLength2; }`; patternLength = `combinePatternLengths(${builder.getStrokePatternLengthExpression()}, ${patternLength})`; } builder.setStrokePatternLengthExpression(patternLength); } } /** * @param {import("../../style/flat.js").FlatStyle} style Style * @param {ShaderBuilder} builder Shader Builder * @param {Object<string,import("../../webgl/Helper").UniformValue>} uniforms Uniforms * @param {import("../../expr/gpu.js").CompilationContext} context Shader compilation context */ function parseFillProperties(style, builder, uniforms, context) { if ('fill-color' in style) { builder.setFillColorExpression( expressionToGlsl(context, style['fill-color'], ColorType), ); } if ('fill-pattern-src' in style) { const textureId = computeHash(style['fill-pattern-src']); const sizeExpression = parseImageProperties( style, builder, uniforms, 'fill-pattern-', textureId, ); let sampleSizeExpression = sizeExpression; let offsetExpression = 'vec2(0.)'; if ('fill-pattern-offset' in style && 'fill-pattern-size' in style) { sampleSizeExpression = expressionToGlsl( context, style[`fill-pattern-size`], NumberArrayType, ); offsetExpression = parseImageOffsetProperties( style, 'fill-pattern-', context, sizeExpression, sampleSizeExpression, ); } context.functions['sampleFillPattern'] = `vec4 sampleFillPattern(sampler2D texture, vec2 textureSize, vec2 textureOffset, vec2 sampleSize, vec2 pxOrigin, vec2 pxPosition) { float scaleRatio = pow(2., mod(u_zoom + 0.5, 1.) - 0.5); vec2 pxRelativePos = pxPosition - pxOrigin; // rotate the relative position from origin by the current view rotation pxRelativePos = vec2(pxRelativePos.x * cos(u_rotation) - pxRelativePos.y * sin(u_rotation), pxRelativePos.x * sin(u_rotation) + pxRelativePos.y * cos(u_rotation)); // sample position is computed according to the sample offset & size vec2 samplePos = mod(pxRelativePos / scaleRatio, sampleSize); // also make sure that we're not sampling too close to the borders to avoid interpolation with outside pixels samplePos = clamp(samplePos, vec2(0.5), sampleSize - vec2(0.5)); samplePos.y = sampleSize.y - samplePos.y; // invert y axis so that images appear upright return texture2D(texture, (samplePos + textureOffset) / textureSize); }`; const textureName = `u_texture${textureId}`; let tintExpression = '1.'; if ('fill-color' in style) { tintExpression = builder.getFillColorExpression(); } builder.setFillColorExpression( `${tintExpression} * sampleFillPattern(${textureName}, ${sizeExpression}, ${offsetExpression}, ${sampleSizeExpression}, pxOrigin, pxPos)`, ); } } /** * @typedef {Object} StyleParseResult * @property {ShaderBuilder} builder Shader builder pre-configured according to a given style * @property {import("./VectorStyleRenderer.js").UniformDefinitions} uniforms Uniform definitions * @property {import("./VectorStyleRenderer.js").AttributeDefinitions} attributes Attribute definitions */ /** * Parses a {@link import("../../style/flat.js").FlatStyle} object and returns a {@link ShaderBuilder} * object that has been configured according to the given style, as well as `attributes` and `uniforms` * arrays to be fed to the `WebGLPointsRenderer` class. * * Also returns `uniforms` and `attributes` properties as expected by the * {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}. * * @param {import("../../style/flat.js").FlatStyle} style Flat style. * @param {import('../../style/flat.js').StyleVariables} [variables] Style variables. * @param {import("../../expr/expression.js").EncodedExpression} [filter] Filter (if any) * @return {StyleParseResult} Result containing shader params, attributes and uniforms. */ export function parseLiteralStyle(style, variables, filter) { const context = newCompilationContext(); const builder = new ShaderBuilder(); /** @type {Object<string,import("../../webgl/Helper").UniformValue>} */ const uniforms = {}; if ('icon-src' in style) { parseIconProperties(style, builder, uniforms, context); } else if ('shape-points' in style) { parseShapeProperties(style, builder, uniforms, context); } else if ('circle-radius' in style) { parseCircleProperties(style, builder, uniforms, context); } parseStrokeProperties(style, builder, uniforms, context); parseFillProperties(style, builder, uniforms, context); // note that the style filter may have already been applied earlier when building the rendering instructions // this is still needed in case a filter cannot be evaluated statically beforehand (e.g. depending on time) if (filter) { const parsedFilter = expressionToGlsl(context, filter, BooleanType); builder.setFragmentDiscardExpression(`!${parsedFilter}`); } /** * @type {import('./VectorStyleRenderer.js').AttributeDefinitions} */ const attributes = {}; // Define attributes for special inputs function defineSpecialInput(contextPropName, glslPropName, type, callback) { if (!context[contextPropName]) { return; } const glslType = getGlslTypeFromType(type); const attrSize = getGlslSizeFromType(type); builder.addAttribute(`a_${glslPropName}`, glslType); attributes[glslPropName] = { size: attrSize, callback, }; } defineSpecialInput( 'geometryType', GEOMETRY_TYPE_PROPERTY_NAME, StringType, (feature) => getStringNumberEquivalent(computeGeometryType(feature.getGeometry())), ); defineSpecialInput( 'featureId', FEATURE_ID_PROPERTY_NAME, StringType | NumberType, (feature) => { const id = feature.getId() ?? null; return typeof id === 'string' ? getStringNumberEquivalent(id) : id; }, ); applyContextToBuilder(builder, context); return { builder, attributes: {...attributes, ...generateAttributesFromContext(context)}, uniforms: { ...uniforms, ...generateUniformsFromContext(context, variables), }, }; }