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
+ ]
+ ]
+ }
+ }
+ }
+ ]
+}