Skip to content

Commit

Permalink
implement stretchable icons for icon-text-fit (#8997)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansis authored Nov 28, 2019
1 parent 009fecf commit 5255736
Show file tree
Hide file tree
Showing 30 changed files with 1,885 additions and 73 deletions.
129 changes: 129 additions & 0 deletions debug/stretchable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>

<body>
<div id='map'></div>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>
<script>

var map = window.map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v10',
hash: true
});

const border = 16;

map.on('styleimagemissing', function(e) {
var id = e.id; // id of the missing image

// check if this missing icon is one this function can generate
var prefix = 'stretchable-';
if (id.indexOf(prefix) !== 0) return;

// extract the color from the id
var rgb1 = id.replace(prefix, '').split(',').map(Number);

var width = 64; // The image will be 64 pixels square
var bytesPerPixel = 4; // Each pixel is represented by 4 bytes: red, green, blue, and alpha.
var data = new Uint8Array(width * width * bytesPerPixel);

const rgb2 = [rgb1[2], rgb1[1], rgb1[0]];

for (var x = 0; x < width; x++) {
for (var y = 0; y < width; y++) {
var offset = (y * width + x) * bytesPerPixel;

let rgb =
x < border || x + border > width - 1 ||
y < border || y + border > width - 1 ?
rgb2 :
rgb1;

const half = width / 2;
if (Math.abs(x - half) + Math.abs(y - half) < border / 2) {
rgb = [255, 255, 255];
}

data[offset + 0] = rgb[0]; // red
data[offset + 1] = rgb[1]; // green
data[offset + 2] = rgb[2]; // blue
data[offset + 3] = 255; // alpha
}
}

map.addImage(id, {width, height: width, data}, {
content: [border, border, width - border, width - border],
stretchX: [[border, (width - border) / 2], [(width + border) / 2, width - border]],
stretchY: [[border, (width - border) / 2], [(width + border) / 2, width - border]]
});
});

map.on('load', function () {
map.addLayer({
"id": "points",
"type": "symbol",
"source": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [0, 0]
},
"properties": {
"text": "ASDF",
"size": 36,
"color": "255,0,0"
}
}, {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [50, 0]
},
"properties": {
"text": "ASDF \n - \nqwer",
"size": 24,
"color": "255,209,28"
}
}, {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-50, 0]
},
"properties": {
"text": "ASDF",
"size": 18,
"color": "242,127,32"
}
}]
}
},
"layout": {
"text-field": ["get", "text"],
"icon-text-fit": "both",
"text-size": ["get", "size"],
"icon-image": ["concat", "stretchable-", ["get", "color"]]
}
});
});

</script>
</body>
</html>
23 changes: 14 additions & 9 deletions src/data/array_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,11 @@ register('StructArrayLayout8ui16', StructArrayLayout8ui16);
* Implementation of the StructArray layout:
* [0]: Int16[4]
* [8]: Uint16[4]
* [16]: Int16[4]
*
* @private
*/
class StructArrayLayout4i4ui16 extends StructArray {
class StructArrayLayout4i4ui4i24 extends StructArray {
uint8: Uint8Array;
int16: Int16Array;
uint16: Uint16Array;
Expand All @@ -203,14 +204,14 @@ class StructArrayLayout4i4ui16 extends StructArray {
this.uint16 = new Uint16Array(this.arrayBuffer);
}

emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number) {
const i = this.length;
this.resize(i + 1);
return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7);
return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11);
}

emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
const o2 = i * 8;
emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number) {
const o2 = i * 12;
this.int16[o2 + 0] = v0;
this.int16[o2 + 1] = v1;
this.int16[o2 + 2] = v2;
Expand All @@ -219,12 +220,16 @@ class StructArrayLayout4i4ui16 extends StructArray {
this.uint16[o2 + 5] = v5;
this.uint16[o2 + 6] = v6;
this.uint16[o2 + 7] = v7;
this.int16[o2 + 8] = v8;
this.int16[o2 + 9] = v9;
this.int16[o2 + 10] = v10;
this.int16[o2 + 11] = v11;
return i;
}
}

StructArrayLayout4i4ui16.prototype.bytesPerElement = 16;
register('StructArrayLayout4i4ui16', StructArrayLayout4i4ui16);
StructArrayLayout4i4ui4i24.prototype.bytesPerElement = 24;
register('StructArrayLayout4i4ui4i24', StructArrayLayout4i4ui4i24);

/**
* Implementation of the StructArray layout:
Expand Down Expand Up @@ -1143,7 +1148,7 @@ export {
StructArrayLayout2i4i12,
StructArrayLayout2i4ub8,
StructArrayLayout8ui16,
StructArrayLayout4i4ui16,
StructArrayLayout4i4ui4i24,
StructArrayLayout3f12,
StructArrayLayout1ul4,
StructArrayLayout6i1ul2ui2i24,
Expand All @@ -1167,7 +1172,7 @@ export {
StructArrayLayout2i4 as HeatmapLayoutArray,
StructArrayLayout2i4ub8 as LineLayoutArray,
StructArrayLayout8ui16 as PatternLayoutArray,
StructArrayLayout4i4ui16 as SymbolLayoutArray,
StructArrayLayout4i4ui4i24 as SymbolLayoutArray,
StructArrayLayout3f12 as SymbolDynamicLayoutArray,
StructArrayLayout1ul4 as SymbolOpacityArray,
StructArrayLayout2i2i2i12 as CollisionBoxLayoutArray,
Expand Down
5 changes: 3 additions & 2 deletions src/data/bucket/symbol_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import {createLayout} from '../../util/struct_array';

export const symbolLayoutAttributes = createLayout([
{name: 'a_pos_offset', components: 4, type: 'Int16'},
{name: 'a_data', components: 4, type: 'Uint16'}
]);
{name: 'a_data', components: 4, type: 'Uint16'},
{name: 'a_pixeloffset', components: 4, type: 'Int16'}
], 4);

export const dynamicLayoutAttributes = createLayout([
{name: 'a_projected_pos', components: 3, type: 'Float32'}
Expand Down
22 changes: 15 additions & 7 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const shaderOpacityAttributes = [
{name: 'a_fade_opacity', components: 1, type: 'Uint8', offset: 0}
];

function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex, isSDF: boolean) {
function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex, isSDF: boolean, pixelOffsetX, pixelOffsetY, minFontScaleX, minFontScaleY) {
const aSizeX = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[0])) : 0;
const aSizeY = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[1])) : 0;
array.emplaceBack(
Expand All @@ -116,7 +116,11 @@ function addVertex(array, anchorX, anchorY, ox, oy, tx, ty, sizeVertex, isSDF: b
tx, // x coordinate of symbol on glyph atlas texture
ty, // y coordinate of symbol on glyph atlas texture
(aSizeX << 1) + (isSDF ? 1 : 0),
aSizeY
aSizeY,
pixelOffsetX * 16,
pixelOffsetY * 16,
minFontScaleX * 256,
minFontScaleY * 256
);
}

Expand Down Expand Up @@ -593,15 +597,19 @@ class SymbolBucket implements Bucket {
tr = symbol.tr,
bl = symbol.bl,
br = symbol.br,
tex = symbol.tex;
tex = symbol.tex,
pixelOffsetTL = symbol.pixelOffsetTL,
pixelOffsetBR = symbol.pixelOffsetBR,
mfsx = symbol.minFontScaleX,
mfsy = symbol.minFontScaleY;

const index = segment.vertexLength;

const y = symbol.glyphOffset[1];
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex, symbol.isSDF);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex, symbol.isSDF);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex, symbol.isSDF);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, symbol.isSDF);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex, symbol.isSDF, pixelOffsetTL.x, pixelOffsetTL.y, mfsx, mfsy);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex, symbol.isSDF, pixelOffsetBR.x, pixelOffsetTL.y, mfsx, mfsy);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex, symbol.isSDF, pixelOffsetTL.x, pixelOffsetBR.y, mfsx, mfsy);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, symbol.isSDF, pixelOffsetBR.x, pixelOffsetBR.y, mfsx, mfsy);

addDynamicAttributes(dynamicLayoutVertexArray, labelAnchor, angle);

Expand Down
10 changes: 8 additions & 2 deletions src/render/image_atlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {StyleImage} from '../style/style_image';
import type ImageManager from './image_manager';
import type Texture from './texture';

const IMAGE_PADDING = 1;
const IMAGE_PADDING: number = 1;
export {IMAGE_PADDING};

type Rect = {
Expand All @@ -22,10 +22,16 @@ export class ImagePosition {
paddedRect: Rect;
pixelRatio: number;
version: number;
stretchY: ?Array<[number, number]>;
stretchX: ?Array<[number, number]>;
content: ?[number, number, number, number];

constructor(paddedRect: Rect, {pixelRatio, version}: StyleImage) {
constructor(paddedRect: Rect, {pixelRatio, version, stretchX, stretchY, content}: StyleImage) {
this.paddedRect = paddedRect;
this.pixelRatio = pixelRatio;
this.stretchX = stretchX;
this.stretchY = stretchY;
this.content = content;
this.version = version;
}

Expand Down
48 changes: 46 additions & 2 deletions src/render/image_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import potpack from 'potpack';

import {Event, Evented} from '../util/evented';
import {Event, ErrorEvent, Evented} from '../util/evented';
import {RGBAImage} from '../util/image';
import {ImagePosition} from './image_atlas';
import Texture from './texture';
Expand Down Expand Up @@ -87,7 +87,48 @@ class ImageManager extends Evented {

addImage(id: string, image: StyleImage) {
assert(!this.images[id]);
this.images[id] = image;
if (this._validate(id, image)) {
this.images[id] = image;
}
}

_validate(id: string, image: StyleImage) {
let valid = true;
if (!this._validateStretch(image.stretchX, image.data && image.data.width)) {
this.fire(new ErrorEvent(new Error(`Image "${id}" has invalid "stretchX" value`)));
valid = false;
}
if (!this._validateStretch(image.stretchY, image.data && image.data.height)) {
this.fire(new ErrorEvent(new Error(`Image "${id}" has invalid "stretchY" value`)));
valid = false;
}
if (!this._validateContent(image.content, image)) {
this.fire(new ErrorEvent(new Error(`Image "${id}" has invalid "content" value`)));
valid = false;
}
return valid;
}

_validateStretch(stretch: ?Array<[number, number]> | void, size: number) {
if (!stretch) return true;
let last = 0;
for (const part of stretch) {
if (part[0] < last || part[1] < part[0] || size < part[1]) return false;
last = part[1];
}
return true;
}

_validateContent(content: ?[number, number, number, number] | void, image: StyleImage) {
if (!content) return true;
if (content.length !== 4) return false;
if (content[0] < 0 || image.data.width < content[0]) return false;
if (content[1] < 0 || image.data.height < content[1]) return false;
if (content[2] < 0 || image.data.width < content[2]) return false;
if (content[3] < 0 || image.data.height < content[3]) return false;
if (content[2] < content[0]) return false;
if (content[3] < content[1]) return false;
return true;
}

updateImage(id: string, image: StyleImage) {
Expand Down Expand Up @@ -150,6 +191,9 @@ class ImageManager extends Evented {
pixelRatio: image.pixelRatio,
sdf: image.sdf,
version: image.version,
stretchX: image.stretchX,
stretchY: image.stretchY,
content: image.content,
hasRenderCallback: Boolean(image.userImage && image.userImage.render)
};
} else {
Expand Down
6 changes: 5 additions & 1 deletion src/shaders/symbol_icon.vertex.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const float PI = 3.141592653589793;

attribute vec4 a_pos_offset;
attribute vec4 a_data;
attribute vec4 a_pixeloffset;
attribute vec3 a_projected_pos;
attribute float a_fade_opacity;

Expand Down Expand Up @@ -39,6 +40,9 @@ void main() {
vec2 a_size = a_data.zw;

float a_size_min = floor(a_size[0] * 0.5);
vec2 a_pxoffset = a_pixeloffset.xy;
vec2 a_minFontScale = a_pixeloffset.zw / 256.0;

highp float segment_angle = -a_projected_pos[2];
float size;

Expand Down Expand Up @@ -81,7 +85,7 @@ void main() {
mat2 rotation_matrix = mat2(angle_cos, -1.0 * angle_sin, angle_sin, angle_cos);

vec4 projected_pos = u_label_plane_matrix * vec4(a_projected_pos.xy, 0.0, 1.0);
gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * fontScale), 0.0, 1.0);
gl_Position = u_coord_matrix * vec4(projected_pos.xy / projected_pos.w + rotation_matrix * (a_offset / 32.0 * max(a_minFontScale, fontScale) + a_pxoffset / 16.0), 0.0, 1.0);

v_tex = a_tex / u_texsize;
vec2 fade_opacity = unpack_opacity(a_fade_opacity);
Expand Down
Loading

0 comments on commit 5255736

Please sign in to comment.