diff --git a/debug/textsize.html b/debug/textsize.html new file mode 100644 index 00000000000..185897ce285 --- /dev/null +++ b/debug/textsize.html @@ -0,0 +1,120 @@ + + + + Mapbox GL JS debug page + + + + + + + +
+ + + + + + diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 148351f020c..2bbd0a51431 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -42,6 +42,7 @@ const symbolInterfaces = { layoutVertexArrayType: layoutVertexArrayType, elementArrayType: elementArrayType, paintAttributes: [ + {name: 'a_size', property: 'text-size', type: 'Uint16', multiplier: 10}, {name: 'a_fill_color', property: 'text-color', type: 'Uint8'}, {name: 'a_halo_color', property: 'text-halo-color', type: 'Uint8'}, {name: 'a_halo_width', property: 'text-halo-width', type: 'Uint16', multiplier: 10}, @@ -53,6 +54,7 @@ const symbolInterfaces = { layoutVertexArrayType: layoutVertexArrayType, elementArrayType: elementArrayType, paintAttributes: [ + {name: 'a_size', property: 'icon-size', type: 'Uint16', multiplier: 10}, {name: 'a_fill_color', property: 'icon-color', type: 'Uint8'}, {name: 'a_halo_color', property: 'icon-halo-color', type: 'Uint8'}, {name: 'a_halo_width', property: 'icon-halo-width', type: 'Uint16', multiplier: 10}, @@ -138,8 +140,6 @@ class SymbolBucket { this.index = options.index; this.sdfIcons = options.sdfIcons; this.iconsNeedLinear = options.iconsNeedLinear; - this.adjustedTextSize = options.adjustedTextSize; - this.adjustedIconSize = options.adjustedIconSize; this.fontstack = options.fontstack; if (options.arrays) { @@ -245,8 +245,6 @@ class SymbolBucket { layerIds: this.layers.map((l) => l.id), sdfIcons: this.sdfIcons, iconsNeedLinear: this.iconsNeedLinear, - adjustedTextSize: this.adjustedTextSize, - adjustedIconSize: this.adjustedIconSize, fontstack: this.fontstack, arrays: util.mapObject(this.arrays, (a) => a.isEmpty() ? null : a.serialize(transferables)) }; @@ -270,15 +268,6 @@ class SymbolBucket { prepare(stacks, icons) { this.symbolInstances = []; - // To reduce the number of labels that jump around when zooming we need - // to use a text-size value that is the same for all zoom levels. - // This calculates text-size at a high zoom level so that all tiles can - // use the same value when calculating anchor positions. - this.adjustedTextMaxSize = this.layers[0].getLayoutValue('text-size', {zoom: 18}); - this.adjustedTextSize = this.layers[0].getLayoutValue('text-size', {zoom: this.zoom + 1}); - this.adjustedIconMaxSize = this.layers[0].getLayoutValue('icon-size', {zoom: 18}); - this.adjustedIconSize = this.layers[0].getLayoutValue('icon-size', {zoom: this.zoom + 1}); - const tileSize = 512 * this.overscaling; this.tilePixelRatio = EXTENT / tileSize; this.compareText = {}; @@ -368,13 +357,21 @@ class SymbolBucket { } addFeature(feature, shapedTextOrientations, shapedIcon) { + // To reduce the number of labels that jump around when zooming we need + // to use a text-size value that is the same for all zoom levels. + // This calculates text-size at a high zoom level so that all tiles can + // use the same value when calculating anchor positions. + const adjustedTextSize = this.layers[0].getLayoutValue('text-size', {zoom: this.zoom + 1}, feature.properties); + const adjustedIconSize = this.layers[0].getLayoutValue('icon-size', {zoom: this.zoom + 1}, feature.properties); + const adjustedTextMaxSize = this.layers[0].getLayoutValue('text-size', {zoom: 18}, feature.properties); + const layout = this.layers[0].layout, glyphSize = 24, - fontScale = this.adjustedTextSize / glyphSize, - textMaxSize = this.adjustedTextMaxSize !== undefined ? this.adjustedTextMaxSize : this.adjustedTextSize, + fontScale = adjustedTextSize / glyphSize, + textMaxSize = adjustedTextMaxSize !== undefined ? adjustedTextMaxSize : adjustedTextSize, textBoxScale = this.tilePixelRatio * fontScale, textMaxBoxScale = this.tilePixelRatio * textMaxSize / glyphSize, - iconBoxScale = this.tilePixelRatio * this.adjustedIconSize, + iconBoxScale = this.tilePixelRatio * adjustedIconSize, symbolMinDistance = this.tilePixelRatio * layout['symbol-spacing'], avoidEdges = layout['symbol-avoid-edges'], textPadding = layout['text-padding'] * this.tilePixelRatio, diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index ac11affda46..85f05612b05 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -1,5 +1,6 @@ 'use strict'; +const assert = require('assert'); const browser = require('../util/browser'); const drawCollisionDebug = require('./draw_collision_debug'); const pixelsToTileUnits = require('../source/pixels_to_tile_units'); @@ -37,16 +38,14 @@ function drawSymbols(painter, sourceCache, layer, coords) { layer.layout['icon-rotation-alignment'], // icon-pitch-alignment is not yet implemented // and we simply inherit the rotation alignment - layer.layout['icon-rotation-alignment'], - layer.layout['icon-size'] + layer.layout['icon-rotation-alignment'] ); drawLayerSymbols(painter, sourceCache, layer, coords, true, layer.paint['text-translate'], layer.paint['text-translate-anchor'], layer.layout['text-rotation-alignment'], - layer.layout['text-pitch-alignment'], - layer.layout['text-size'] + layer.layout['text-pitch-alignment'] ); if (sourceCache.map.showCollisionBoxes) { @@ -55,7 +54,7 @@ function drawSymbols(painter, sourceCache, layer, coords) { } function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate, translateAnchor, - rotationAlignment, pitchAlignment, size) { + rotationAlignment, pitchAlignment) { if (!isText && painter.style.sprite && !painter.style.sprite.loaded()) return; @@ -90,8 +89,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate program = painter.useProgram(isSDF ? 'symbolSDF' : 'symbolIcon', programConfiguration); programConfiguration.setUniforms(gl, program, layer, {zoom: painter.transform.zoom}); - setSymbolDrawState(program, painter, isText, isSDF, rotateWithMap, pitchWithMap, bucket.fontstack, size, - bucket.iconsNeedLinear, isText ? bucket.adjustedTextSize : bucket.adjustedIconSize); + setSymbolDrawState(program, painter, layer, coord.z, isText, isSDF, rotateWithMap, pitchWithMap, bucket.fontstack, bucket.iconsNeedLinear); } painter.enableTileClippingMask(coord); @@ -99,8 +97,8 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate gl.uniformMatrix4fv(program.u_matrix, false, painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor)); - drawTileSymbols(program, painter, layer, tile, buffers, isText, isSDF, - pitchWithMap, size); + drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, + pitchWithMap); prevFontstack = bucket.fontstack; } @@ -108,8 +106,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate if (!depthOn) gl.enable(gl.DEPTH_TEST); } -function setSymbolDrawState(program, painter, isText, isSDF, rotateWithMap, pitchWithMap, fontstack, size, - iconsNeedLinear, adjustedSize) { +function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, rotateWithMap, pitchWithMap, fontstack, iconsNeedLinear) { const gl = painter.gl; const tr = painter.transform; @@ -120,6 +117,8 @@ function setSymbolDrawState(program, painter, isText, isSDF, rotateWithMap, pitc gl.activeTexture(gl.TEXTURE0); gl.uniform1i(program.u_texture, 0); + gl.uniform1f(program.u_is_text, isText ? 1 : 0); + if (isText) { // use the fonstack used when parsing the tile, not the fontstack // at the current zoom level (layout['text-font']). @@ -130,7 +129,10 @@ function setSymbolDrawState(program, painter, isText, isSDF, rotateWithMap, pitc gl.uniform2f(program.u_texsize, glyphAtlas.width / 4, glyphAtlas.height / 4); } else { const mapMoving = painter.options.rotating || painter.options.zooming; - const iconScaled = size !== 1 || browser.devicePixelRatio !== painter.spriteAtlas.pixelRatio || iconsNeedLinear; + const iconSizeScaled = !layer.isLayoutValueFeatureConstant('text-size') || + !layer.isLayoutValueZoomConstant('text-size') || + layer.getLayoutValue('text-size', { zoom: tr.zoom }, {}) !== 1; + const iconScaled = iconSizeScaled || browser.devicePixelRatio !== painter.spriteAtlas.pixelRatio || iconsNeedLinear; const iconTransformed = pitchWithMap || tr.pitch; painter.spriteAtlas.bind(gl, isSDF || mapMoving || iconScaled || iconTransformed); gl.uniform2f(program.u_texsize, painter.spriteAtlas.width / 4, painter.spriteAtlas.height / 4); @@ -141,35 +143,61 @@ function setSymbolDrawState(program, painter, isText, isSDF, rotateWithMap, pitc gl.uniform1i(program.u_fadetexture, 1); // adjust min/max zooms for variable font sizes - const zoomAdjust = Math.log(size / adjustedSize) / Math.LN2 || 0; - gl.uniform1f(program.u_zoom, (tr.zoom - zoomAdjust) * 10); // current zoom level + gl.uniform1f(program.u_zoom, tr.zoom); gl.uniform1f(program.u_pitch, tr.pitch / 360 * 2 * Math.PI); gl.uniform1f(program.u_bearing, tr.bearing / 360 * 2 * Math.PI); gl.uniform1f(program.u_aspect_ratio, tr.width / tr.height); } -function drawTileSymbols(program, painter, layer, tile, buffers, isText, isSDF, - pitchWithMap, size) { +function drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, + pitchWithMap) { const gl = painter.gl; const tr = painter.transform; - const fontScale = size / (isText ? 24 : 1); + // If {text,icon}-size is a composite function, the shader needs to + // evaluate it at the zoom level that was used at *layout* time, which + // is distinct the current *rendered* zoom level. + const sizeProperty = isText ? 'text-size' : 'icon-size'; + const isSizeCompositeFunction = + !layer.isLayoutValueZoomConstant(sizeProperty) && + !layer.isLayoutValueFeatureConstant(sizeProperty); + gl.uniform1f(program.u_is_size_composite, isSizeCompositeFunction ? 1 : 0); + + const layoutZoom = tile.coord.z + 1; + if (isSizeCompositeFunction) { + // Reproduce the ProgramConfiguration logic to provide the shader with + // an interpolation "t" value corresponding to the layout zoom level. + // (see ProgramConfiguration#addZoomAndPropertyAttribute.) + let stopOffset; + for (const uniform of programConfiguration.interpolationUniforms) { + if (uniform.property === sizeProperty) stopOffset = uniform.stopOffset; + } + assert(typeof stopOffset === 'number'); + const stopInterp = layer.getLayoutInterpolationT(sizeProperty, { zoom: layoutZoom }); + const interpolationT = Math.max(0, Math.min(3, stopInterp - stopOffset)); + gl.uniform1f(program.u_adjusted_size_t, interpolationT); + } else { + gl.uniform1f(program.u_adjusted_size, + layer.getLayoutValue(sizeProperty, { zoom: layoutZoom }) + ); + } if (pitchWithMap) { - const s = pixelsToTileUnits(tile, fontScale, tr.zoom); + const s = pixelsToTileUnits(tile, 1, tr.zoom); gl.uniform2f(program.u_extrude_scale, s, s); } else { - const s = tr.cameraToCenterDistance * fontScale; - gl.uniform2f(program.u_extrude_scale, tr.pixelsToGLUnits[0] * s, tr.pixelsToGLUnits[1] * s); + const s = tr.cameraToCenterDistance; + gl.uniform2f(program.u_extrude_scale, + tr.pixelsToGLUnits[0] * s, + tr.pixelsToGLUnits[1] * s); } if (isSDF) { const haloWidthProperty = `${isText ? 'text' : 'icon'}-halo-width`; const hasHalo = !layer.isPaintValueFeatureConstant(haloWidthProperty) || layer.paint[haloWidthProperty]; - const gammaScale = fontScale * (pitchWithMap ? Math.cos(tr._pitch) : 1) * tr.cameraToCenterDistance; - gl.uniform1f(program.u_font_scale, fontScale); + const gammaScale = (isText ? 1 / 24 : 1) * (pitchWithMap ? Math.cos(tr._pitch) : 1) * tr.cameraToCenterDistance; gl.uniform1f(program.u_gamma_scale, gammaScale); if (hasHalo) { // Draw halo underneath the text. diff --git a/src/shaders/symbol_sdf.fragment.glsl b/src/shaders/symbol_sdf.fragment.glsl index d91605fdf11..09f4f49ab3d 100644 --- a/src/shaders/symbol_sdf.fragment.glsl +++ b/src/shaders/symbol_sdf.fragment.glsl @@ -2,6 +2,7 @@ #define EDGE_GAMMA 0.105/DEVICE_PIXEL_RATIO uniform bool u_is_halo; +#pragma mapbox: define mediump float size #pragma mapbox: define lowp vec4 fill_color #pragma mapbox: define lowp vec4 halo_color #pragma mapbox: define lowp float opacity @@ -10,27 +11,30 @@ uniform bool u_is_halo; uniform sampler2D u_texture; uniform sampler2D u_fadetexture; -uniform lowp float u_font_scale; uniform highp float u_gamma_scale; +uniform bool u_is_text; varying vec2 v_tex; varying vec2 v_fade_tex; varying float v_gamma_scale; void main() { + #pragma mapbox: initialize mediump float size #pragma mapbox: initialize lowp vec4 fill_color #pragma mapbox: initialize lowp vec4 halo_color #pragma mapbox: initialize lowp float opacity #pragma mapbox: initialize lowp float halo_width #pragma mapbox: initialize lowp float halo_blur + float fontScale = u_is_text ? size / 24.0 : size; + lowp vec4 color = fill_color; - highp float gamma = EDGE_GAMMA / u_gamma_scale; + highp float gamma = EDGE_GAMMA / (size * u_gamma_scale); lowp float buff = (256.0 - 64.0) / 256.0; if (u_is_halo) { color = halo_color; - gamma = (halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / u_gamma_scale; - buff = (6.0 - halo_width / u_font_scale) / SDF_PX; + gamma = (halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (size * u_gamma_scale); + buff = (6.0 - halo_width / fontScale) / SDF_PX; } lowp float dist = texture2D(u_texture, v_tex).a; diff --git a/src/shaders/symbol_sdf.vertex.glsl b/src/shaders/symbol_sdf.vertex.glsl index 8bde50b7a12..19a312acbee 100644 --- a/src/shaders/symbol_sdf.vertex.glsl +++ b/src/shaders/symbol_sdf.vertex.glsl @@ -4,6 +4,7 @@ attribute vec4 a_pos_offset; attribute vec2 a_texture_pos; attribute vec4 a_data; +#pragma mapbox: define mediump float size #pragma mapbox: define lowp vec4 fill_color #pragma mapbox: define lowp vec4 halo_color #pragma mapbox: define lowp float opacity @@ -13,6 +14,7 @@ attribute vec4 a_data; // matrix is for the vertex position. uniform mat4 u_matrix; +uniform bool u_is_text; uniform mediump float u_zoom; uniform bool u_rotate_with_map; uniform bool u_pitch_with_map; @@ -21,13 +23,24 @@ uniform mediump float u_bearing; uniform mediump float u_aspect_ratio; uniform vec2 u_extrude_scale; +uniform bool u_is_size_composite; +uniform mediump float u_adjusted_size; // used when size is a camera function +uniform mediump float u_adjusted_size_t; // used when size is a composite function + uniform vec2 u_texsize; varying vec2 v_tex; varying vec2 v_fade_tex; varying float v_gamma_scale; +// Dummy overload so that evaluate_zoom_function_1(a_size, ...) call below +// compiles when {text,icon}-size is not a composite function. +float evaluate_zoom_function_1(float size, float adjusted_size_t) { + return -1.0; +} + void main() { + #pragma mapbox: initialize mediump float size #pragma mapbox: initialize lowp vec4 fill_color #pragma mapbox: initialize lowp vec4 halo_color #pragma mapbox: initialize lowp float opacity @@ -43,8 +56,23 @@ void main() { mediump float a_minzoom = a_zoom[0]; mediump float a_maxzoom = a_zoom[1]; - // u_zoom is the current zoom level adjusted for the change in font size - mediump float z = 2.0 - step(a_minzoom, u_zoom) - (1.0 - step(a_maxzoom, u_zoom)); + float fontScale = u_is_text ? size / 24.0 : size; + + // In order to accommodate placing labels around corners in + // symbol-placement: line, each glyph in a label could have multiple layout + // "instances", only one of which should be shown at a given zoom level. + // The min/max zoom assigned to each instance is based on the font size at + // the vector tile's zoom level, which might be different than at the + // currently rendered zoom level if text-size is zoom-dependent. + // Thus, we compensate for this difference by calculating an adjustment + // based on the scale of rendered text size relative to layout text size. + mediump float adjustedSize = u_is_size_composite + ? evaluate_zoom_function_1(a_size, u_adjusted_size_t) / 10.0 + : u_adjusted_size; + mediump float zoomAdjust = log2(size / adjustedSize); + mediump float adjustedZoom = (u_zoom - zoomAdjust) * 10.0; + // result: z = 0 if a_minzoom <= adjustedZoom < a_maxzoom, and 1 otherwise + mediump float z = 2.0 - step(a_minzoom, adjustedZoom) - (1.0 - step(a_maxzoom, adjustedZoom)); // pitch-alignment: map // rotation-alignment: map | viewport @@ -54,7 +82,7 @@ void main() { lowp float acos = cos(angle); mat2 RotationMatrix = mat2(acos, asin, -1.0 * asin, acos); vec2 offset = RotationMatrix * a_offset; - vec2 extrude = u_extrude_scale * (offset / 64.0); + vec2 extrude = fontScale * u_extrude_scale * (offset / 64.0); gl_Position = u_matrix * vec4(a_pos + extrude, 0, 1); gl_Position.z += z * gl_Position.w; // pitch-alignment: viewport @@ -78,13 +106,13 @@ void main() { mat2 RotationMatrix = mat2(acos, -1.0 * asin, asin, acos); vec2 offset = RotationMatrix * (vec2((1.0-pitchfactor)+(pitchfactor*cos(angle*2.0)), 1.0) * a_offset); - vec2 extrude = u_extrude_scale * (offset / 64.0); + vec2 extrude = fontScale * u_extrude_scale * (offset / 64.0); gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0); gl_Position.z += z * gl_Position.w; // pitch-alignment: viewport // rotation-alignment: viewport } else { - vec2 extrude = u_extrude_scale * (a_offset / 64.0); + vec2 extrude = fontScale * u_extrude_scale * (a_offset / 64.0); gl_Position = u_matrix * vec4(a_pos, 0, 1) + vec4(extrude, 0, 0); } diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index fb2aa587655..826cfb91037 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -1125,6 +1125,7 @@ "units": "pixels", "function": "interpolated", "zoom-function": true, + "property-function": true, "doc": "Font size.", "requires": [ "text-field" diff --git a/test/integration/render-tests/text-size/property-function/expected.png b/test/integration/render-tests/text-size/property-function/expected.png new file mode 100644 index 00000000000..5df868f5e23 Binary files /dev/null and b/test/integration/render-tests/text-size/property-function/expected.png differ diff --git a/test/integration/render-tests/text-size/property-function/style.json b/test/integration/render-tests/text-size/property-function/style.json new file mode 100644 index 00000000000..91e96fc4dc1 --- /dev/null +++ b/test/integration/render-tests/text-size/property-function/style.json @@ -0,0 +1,66 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64 + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "x": 0 }, + "geometry": { + "type": "Point", + "coordinates": [ -10, 0 ] + } + }, + { + "type": "Feature", + "properties": { "x": 5 }, + "geometry": { + "type": "Point", + "coordinates": [ + 10, + 0 + ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "symbol", + "type": "symbol", + "source": "geojson", + "layout": { + "text-field": "A", + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": { + "property": "x", + "stops": [ + [ + 0, + 12 + ], + [ + 10, + 24 + ] + ] + } + } + } + ] +}