@tangram.gl/basemap-layers 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/THIRD-PARTY-NOTICES.md +205 -0
- package/dist/geojson-worker.js +2160 -0
- package/dist/geojson-worker.js.map +6 -0
- package/dist/index.cjs +11388 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.js +11369 -0
- package/dist/index.js.map +6 -0
- package/dist/map-style.cjs +445 -0
- package/dist/map-style.cjs.map +6 -0
- package/dist/map-style.js +431 -0
- package/dist/map-style.js.map +6 -0
- package/dist/types/arabic-shaping.d.ts +7 -0
- package/dist/types/atmosphere-layer.d.ts +10 -0
- package/dist/types/basemap-layer.d.ts +150 -0
- package/dist/types/bidi.d.ts +8 -0
- package/dist/types/circle-layer.d.ts +49 -0
- package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
- package/dist/types/fill-outline-extension.d.ts +22 -0
- package/dist/types/fill-pattern.d.ts +99 -0
- package/dist/types/fill-style-extensions.d.ts +100 -0
- package/dist/types/geojson-source.d.ts +29 -0
- package/dist/types/geojson-tiler.d.ts +48 -0
- package/dist/types/geojson-worker.d.ts +40 -0
- package/dist/types/globe-horizon-extension.d.ts +61 -0
- package/dist/types/globe-layers.d.ts +239 -0
- package/dist/types/globe-line-angle-extension.d.ts +37 -0
- package/dist/types/glyph-font-renderer.d.ts +37 -0
- package/dist/types/glyph-pbf.d.ts +37 -0
- package/dist/types/glyph-store.d.ts +103 -0
- package/dist/types/grid-index.d.ts +47 -0
- package/dist/types/hillshade-layer.d.ts +154 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/is-globe-viewport.d.ts +2 -0
- package/dist/types/label-font-renderer.d.ts +18 -0
- package/dist/types/label-placement.d.ts +247 -0
- package/dist/types/label-text-layer.d.ts +231 -0
- package/dist/types/line-blur-extension.d.ts +73 -0
- package/dist/types/line-glyphs.d.ts +60 -0
- package/dist/types/line-placement.d.ts +93 -0
- package/dist/types/map-style-loader.d.ts +48 -0
- package/dist/types/map-style-schema.d.ts +72 -0
- package/dist/types/map-style.d.ts +6 -0
- package/dist/types/mapbox-style.d.ts +40 -0
- package/dist/types/mvt-label-layer.d.ts +923 -0
- package/dist/types/polylabel.d.ts +17 -0
- package/dist/types/raster-color-extension.d.ts +146 -0
- package/dist/types/shared-texture.d.ts +34 -0
- package/dist/types/sort-key.d.ts +10 -0
- package/dist/types/sprite.d.ts +82 -0
- package/dist/types/style-accessor.d.ts +45 -0
- package/dist/types/style-expression.d.ts +83 -0
- package/dist/types/style-resolver.d.ts +94 -0
- package/dist/types/text-font.d.ts +36 -0
- package/dist/types/text-halo-blur-extension.d.ts +55 -0
- package/dist/types/text-layout.d.ts +71 -0
- package/dist/types/util.d.ts +22 -0
- package/dist/types/zoom-paint-extension.d.ts +72 -0
- package/package.json +80 -3
- package/src/arabic-shaping.ts +366 -0
- package/src/atmosphere-layer.ts +19 -0
- package/src/basemap-layer.ts +533 -0
- package/src/bidi.ts +199 -0
- package/src/circle-layer.ts +191 -0
- package/src/fill-extrusion-lighting-extension.ts +206 -0
- package/src/fill-outline-extension.ts +70 -0
- package/src/fill-pattern.ts +400 -0
- package/src/fill-style-extensions.ts +177 -0
- package/src/geojson-source.ts +318 -0
- package/src/geojson-tiler.ts +185 -0
- package/src/geojson-worker.ts +98 -0
- package/src/globe-horizon-extension.ts +64 -0
- package/src/globe-layers.ts +3487 -0
- package/src/globe-line-angle-extension.ts +131 -0
- package/src/glyph-font-renderer.ts +173 -0
- package/src/glyph-pbf.ts +164 -0
- package/src/glyph-store.ts +326 -0
- package/src/grid-index.ts +177 -0
- package/src/hillshade-layer.ts +775 -0
- package/src/index.ts +18 -0
- package/src/is-globe-viewport.ts +24 -0
- package/src/label-font-renderer.ts +159 -0
- package/src/label-placement.ts +727 -0
- package/src/label-text-layer.ts +913 -0
- package/src/line-blur-extension.ts +237 -0
- package/src/line-glyphs.ts +364 -0
- package/src/line-placement.ts +446 -0
- package/src/map-style-loader.ts +56 -0
- package/src/map-style-schema.ts +83 -0
- package/src/map-style.ts +23 -0
- package/src/mapbox-style.ts +168 -0
- package/src/mvt-label-layer.ts +3744 -0
- package/src/polylabel.ts +312 -0
- package/src/raster-color-extension.ts +245 -0
- package/src/shared-texture.ts +153 -0
- package/src/sort-key.ts +63 -0
- package/src/sprite.ts +244 -0
- package/src/style-accessor.ts +95 -0
- package/src/style-expression.ts +327 -0
- package/src/style-resolver.ts +301 -0
- package/src/text-font.ts +150 -0
- package/src/text-halo-blur-extension.ts +115 -0
- package/src/text-layout.ts +441 -0
- package/src/util.ts +47 -0
- package/src/zoom-paint-extension.ts +207 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {LayerExtension, project32} from '@deck.gl/core';
|
|
6
|
+
import type {Layer} from '@deck.gl/core';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Turns a billboard drawn along a line on a globe from the line's direction at the anchor (the
|
|
10
|
+
* instance angle, counter-clockwise from east) to that direction as the view draws it, so the label
|
|
11
|
+
* follows its line on every frame, at any bearing, anywhere on the globe, without updating any
|
|
12
|
+
* attribute. The direction on screen is the line's tangent at the anchor, east and north on the
|
|
13
|
+
* globe's surface (the derivatives of `project_globe_`), through the view-projection matrix: exact,
|
|
14
|
+
* with no difference of nearby positions to lose precision.
|
|
15
|
+
*
|
|
16
|
+
* With `keepUpright`, a billboard that would read upside down there is turned by another half turn,
|
|
17
|
+
* as `getUprightAngle` turns it from the bearing alone: away from the middle of the view the bearing
|
|
18
|
+
* does not tell which way the view draws a line.
|
|
19
|
+
*
|
|
20
|
+
* With `pixelOffset`, the name of the layer's pixel offset attribute, that offset is not turned
|
|
21
|
+
* around with the billboard: it is a direction along and across the line (see
|
|
22
|
+
* `withLineDirection`), and stays on the same side of the line.
|
|
23
|
+
*
|
|
24
|
+
* The instance angle and pixel offset are only declared in the layer's own shader source, after the
|
|
25
|
+
* hook functions, so they are copied into globals at the start of `main`.
|
|
26
|
+
*/
|
|
27
|
+
const getGlobeLineAngleModule = (keepUpright: boolean, pixelOffset?: string) => ({
|
|
28
|
+
name: `globeLineAngle${keepUpright ? 'Upright' : ''}${pixelOffset ? `_${pixelOffset}` : ''}`,
|
|
29
|
+
dependencies: [project32],
|
|
30
|
+
inject: {
|
|
31
|
+
'vs:#decl': /* glsl */ `
|
|
32
|
+
float globeLineAngle_angle;
|
|
33
|
+
vec2 globeLineAngle_pixelOffset;
|
|
34
|
+
`,
|
|
35
|
+
'vs:#main-start': /* glsl */ `
|
|
36
|
+
globeLineAngle_angle = radians(instanceAngles);
|
|
37
|
+
// As the layer adds it to the billboard's offset, y down.
|
|
38
|
+
globeLineAngle_pixelOffset = ${pixelOffset ? `vec2(${pixelOffset}.x, -${pixelOffset}.y)` : 'vec2(0.0)'};
|
|
39
|
+
`,
|
|
40
|
+
'vs:DECKGL_FILTER_SIZE': /* glsl */ `
|
|
41
|
+
if (project.projectionMode == PROJECTION_MODE_GLOBE) {
|
|
42
|
+
float globeLineAngle_lambda = radians(geometry.worldPosition.x);
|
|
43
|
+
float globeLineAngle_phi = radians(geometry.worldPosition.y);
|
|
44
|
+
vec3 globeLineAngle_east = vec3(cos(globeLineAngle_lambda), sin(globeLineAngle_lambda), 0.0);
|
|
45
|
+
vec3 globeLineAngle_north = vec3(
|
|
46
|
+
-sin(globeLineAngle_lambda) * sin(globeLineAngle_phi),
|
|
47
|
+
cos(globeLineAngle_lambda) * sin(globeLineAngle_phi),
|
|
48
|
+
cos(globeLineAngle_phi)
|
|
49
|
+
);
|
|
50
|
+
vec3 globeLineAngle_tangent = cos(globeLineAngle_angle) * globeLineAngle_east +
|
|
51
|
+
sin(globeLineAngle_angle) * globeLineAngle_north;
|
|
52
|
+
vec4 globeLineAngle_clip = project_common_position_to_clipspace(
|
|
53
|
+
vec4(project_position(geometry.worldPosition), 1.0)
|
|
54
|
+
);
|
|
55
|
+
vec4 globeLineAngle_clipTangent = project.viewProjectionMatrix * vec4(globeLineAngle_tangent, 0.0);
|
|
56
|
+
// The derivative of the anchor's normalized device coordinates, times w squared, in pixels.
|
|
57
|
+
vec2 globeLineAngle_screen = (globeLineAngle_clipTangent.xy * globeLineAngle_clip.w -
|
|
58
|
+
globeLineAngle_clip.xy * globeLineAngle_clipTangent.w) * project.viewportSize;
|
|
59
|
+
float globeLineAngle_turn =
|
|
60
|
+
atan(globeLineAngle_screen.y, globeLineAngle_screen.x) - globeLineAngle_angle;
|
|
61
|
+
float globeLineAngle_cos = cos(globeLineAngle_turn);
|
|
62
|
+
float globeLineAngle_sin = sin(globeLineAngle_turn);
|
|
63
|
+
mat2 globeLineAngle_rotation =
|
|
64
|
+
mat2(globeLineAngle_cos, globeLineAngle_sin, -globeLineAngle_sin, globeLineAngle_cos);
|
|
65
|
+
${
|
|
66
|
+
keepUpright
|
|
67
|
+
? ` if (globeLineAngle_screen.x < 0.0) {
|
|
68
|
+
// Turned around, but for the pixel offset: -(size - offset) + offset.
|
|
69
|
+
size.xy = 2.0 * globeLineAngle_pixelOffset - size.xy;
|
|
70
|
+
}
|
|
71
|
+
`
|
|
72
|
+
: ''
|
|
73
|
+
} size.xy = globeLineAngle_rotation * size.xy;
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const GLOBE_LINE_ANGLE_MODULES = {
|
|
80
|
+
free: getGlobeLineAngleModule(false),
|
|
81
|
+
upright: getGlobeLineAngleModule(true),
|
|
82
|
+
// The pixel offset attributes of `IconLayer` (and the glyphs of `TextLayer`) and of
|
|
83
|
+
// `TextLayer`'s backgrounds.
|
|
84
|
+
instancePixelOffset: getGlobeLineAngleModule(true, 'instancePixelOffset'),
|
|
85
|
+
instancePixelOffsets: getGlobeLineAngleModule(true, 'instancePixelOffsets')
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Keeps billboards drawn along lines (labels and icons whose rotation is aligned to the map) on
|
|
90
|
+
* their lines in a `GlobeView`, which draws them in screen space: their angles are taken from
|
|
91
|
+
* north up, and the shader turns them as the view draws the line, upright with `keepUpright`. In
|
|
92
|
+
* other views it does nothing. Add it only to billboards (the shader would also turn the offset of
|
|
93
|
+
* a layer drawn in the map plane) whose angle is a direction on the map. WebGL only: on WebGPU the
|
|
94
|
+
* extension adds no shader code, and they stay as they are drawn from north up.
|
|
95
|
+
*
|
|
96
|
+
* Added to a composite layer such as `TextLayer`, it reaches the character and background
|
|
97
|
+
* sublayers. With `keepOffsetSide`, text kept upright is turned around without its pixel offset,
|
|
98
|
+
* which must then be a direction along and across the line, as `text-offset` is.
|
|
99
|
+
*/
|
|
100
|
+
export class GlobeLineAngleExtension extends LayerExtension<{
|
|
101
|
+
keepUpright: boolean;
|
|
102
|
+
keepOffsetSide?: boolean;
|
|
103
|
+
}> {
|
|
104
|
+
static extensionName = 'GlobeLineAngleExtension';
|
|
105
|
+
|
|
106
|
+
constructor(opts: {keepUpright: boolean; keepOffsetSide?: boolean}) {
|
|
107
|
+
super(opts);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getShaders(this: Layer, extension: GlobeLineAngleExtension) {
|
|
111
|
+
if (this.context.device.type === 'webgpu') {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
const {keepUpright, keepOffsetSide} = extension.opts;
|
|
115
|
+
if (!keepUpright) {
|
|
116
|
+
return {modules: [GLOBE_LINE_ANGLE_MODULES.free]};
|
|
117
|
+
}
|
|
118
|
+
if (!keepOffsetSide) {
|
|
119
|
+
return {modules: [GLOBE_LINE_ANGLE_MODULES.upright]};
|
|
120
|
+
}
|
|
121
|
+
// `TextLayer`'s backgrounds name the attribute in the plural.
|
|
122
|
+
const attributes = (this.getAttributeManager() as any)?.attributes || {};
|
|
123
|
+
return {
|
|
124
|
+
modules: [
|
|
125
|
+
attributes.instancePixelOffsets
|
|
126
|
+
? GLOBE_LINE_ANGLE_MODULES.instancePixelOffsets
|
|
127
|
+
: GLOBE_LINE_ANGLE_MODULES.instancePixelOffset
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
// deck.gl-community
|
|
6
|
+
// SPDX-License-Identifier: MIT
|
|
7
|
+
// Copyright (c) vis.gl contributors
|
|
8
|
+
|
|
9
|
+
import TinySDF from '@mapbox/tiny-sdf';
|
|
10
|
+
import type {TextLayerProps} from '@deck.gl/layers';
|
|
11
|
+
import {GLYPH_BORDER, GLYPH_EM} from './glyph-pbf';
|
|
12
|
+
import type {Glyph} from './glyph-pbf';
|
|
13
|
+
import {isLocalGlyph} from './glyph-store';
|
|
14
|
+
import type {GlyphStore} from './glyph-store';
|
|
15
|
+
|
|
16
|
+
type GetFontRenderer = NonNullable<TextLayerProps['_getFontRenderer']>;
|
|
17
|
+
type FontRenderer = ReturnType<GetFontRenderer>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `TextLayer`'s font settings for glyphs from a style's `glyphs` URL: distance fields one em of
|
|
21
|
+
* {@link GLYPH_EM} pixels high, reaching 8 pixels, with the edge at 0.75, as MapLibre GL JS draws
|
|
22
|
+
* them. Cells are packed `buffer` pixels apart, the border of the glyph bitmaps.
|
|
23
|
+
*/
|
|
24
|
+
export const GLYPH_FONT_SETTINGS = {
|
|
25
|
+
sdf: true,
|
|
26
|
+
fontSize: GLYPH_EM,
|
|
27
|
+
buffer: GLYPH_BORDER,
|
|
28
|
+
radius: 8,
|
|
29
|
+
cutoff: 0.25,
|
|
30
|
+
smoothing: 0.1
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* MapLibre GL JS sets each line's glyphs this many pixels below the line's top, at one em of
|
|
35
|
+
* {@link GLYPH_EM} pixels (`SHAPING_DEFAULT_OFFSET`, negated).
|
|
36
|
+
*/
|
|
37
|
+
const LINE_GLYPH_OFFSET = 17;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* MapLibre GL JS's alignment of a glyph drawn with the browser's fonts (see `isLocalGlyph`) with
|
|
41
|
+
* the glyphs of glyph ranges, whose `top` is measured from above the em box.
|
|
42
|
+
*/
|
|
43
|
+
const LOCAL_GLYPH_TOP = 27.5;
|
|
44
|
+
const LOCAL_GLYPH_LEFT = 0.5;
|
|
45
|
+
|
|
46
|
+
/** The font-weight MapLibre GL JS draws local glyphs in, from the font stack's names. */
|
|
47
|
+
function getLocalGlyphWeight(fontStack: string): string {
|
|
48
|
+
if (/bold/i.test(fontStack)) return '900';
|
|
49
|
+
if (/medium/i.test(fontStack)) return '500';
|
|
50
|
+
if (/light/i.test(fontStack)) return '200';
|
|
51
|
+
return '400';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const LOCAL_GLYPH_SDFS = new Map<string, TinySDF>();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A glyph that MapLibre GL JS draws with the browser's fonts, as a glyph of a glyph range. MapLibre
|
|
58
|
+
* draws these at twice the resolution; here they share the one-em atlas of the other glyphs.
|
|
59
|
+
*/
|
|
60
|
+
function getLocalGlyph(fontStack: string, codePoint: number): Glyph | null {
|
|
61
|
+
if (typeof document === 'undefined' && typeof OffscreenCanvas === 'undefined') {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const fontWeight = getLocalGlyphWeight(fontStack);
|
|
65
|
+
let sdf = LOCAL_GLYPH_SDFS.get(fontWeight);
|
|
66
|
+
if (!sdf) {
|
|
67
|
+
sdf = new TinySDF({
|
|
68
|
+
fontSize: GLYPH_EM,
|
|
69
|
+
buffer: GLYPH_BORDER,
|
|
70
|
+
radius: GLYPH_FONT_SETTINGS.radius,
|
|
71
|
+
cutoff: GLYPH_FONT_SETTINGS.cutoff,
|
|
72
|
+
fontFamily: 'sans-serif',
|
|
73
|
+
fontWeight
|
|
74
|
+
});
|
|
75
|
+
LOCAL_GLYPH_SDFS.set(fontWeight, sdf);
|
|
76
|
+
}
|
|
77
|
+
const drawn = sdf.draw(String.fromCodePoint(codePoint));
|
|
78
|
+
const width = drawn.glyphWidth;
|
|
79
|
+
const height = drawn.glyphHeight;
|
|
80
|
+
return {
|
|
81
|
+
id: codePoint,
|
|
82
|
+
// TinySDF draws `glyphWidth + 2 * buffer` by `glyphHeight + 2 * buffer`, as a glyph range does.
|
|
83
|
+
bitmap: width && height ? drawn.data : new Uint8Array(0),
|
|
84
|
+
width: width && height ? width : 0,
|
|
85
|
+
height: width && height ? height : 0,
|
|
86
|
+
left: drawn.glyphLeft + LOCAL_GLYPH_LEFT,
|
|
87
|
+
top: drawn.glyphTop - LOCAL_GLYPH_TOP,
|
|
88
|
+
advance: drawn.glyphAdvance
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The glyph a character of `fontStack` is drawn with: from the store's ranges, or with the browser's
|
|
94
|
+
* fonts for the characters MapLibre GL JS draws so. Null while its range loads, and when the range
|
|
95
|
+
* has none: the character is then not drawn, as in MapLibre.
|
|
96
|
+
*/
|
|
97
|
+
export function getStyleGlyph(
|
|
98
|
+
store: GlyphStore,
|
|
99
|
+
fontStack: string,
|
|
100
|
+
character: string
|
|
101
|
+
): Glyph | null {
|
|
102
|
+
const codePoint = character.codePointAt(0);
|
|
103
|
+
if (codePoint === undefined) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (isLocalGlyph(codePoint)) {
|
|
107
|
+
return getLocalGlyph(fontStack, codePoint);
|
|
108
|
+
}
|
|
109
|
+
return store.getGlyph(fontStack, codePoint) ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A function giving a character's advance in ems: the glyph's advance, as MapLibre GL JS lays text
|
|
114
|
+
* out. A character without a glyph advances nothing.
|
|
115
|
+
*/
|
|
116
|
+
export function getGlyphCharacterMeasurer(
|
|
117
|
+
store: GlyphStore,
|
|
118
|
+
fontStack: string
|
|
119
|
+
): (character: string) => number {
|
|
120
|
+
const widths = new Map<string, number>();
|
|
121
|
+
return character => {
|
|
122
|
+
let width = widths.get(character);
|
|
123
|
+
if (width === undefined) {
|
|
124
|
+
const glyph = getStyleGlyph(store, fontStack, character);
|
|
125
|
+
width = (glyph?.advance ?? 0) / GLYPH_EM;
|
|
126
|
+
// A character whose range is still loading is measured again once it has loaded.
|
|
127
|
+
if (glyph || store.hasGlyphs(fontStack, character)) {
|
|
128
|
+
widths.set(character, width);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return width;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* `TextLayer`'s font renderer for the glyphs of a font stack. With
|
|
137
|
+
* {@link GLYPH_FONT_SETTINGS}, `TextLayer` lays each character's glyph out as MapLibre GL JS does:
|
|
138
|
+
* the line's baseline offset puts it {@link LINE_GLYPH_OFFSET} pixels below the line's top, and the
|
|
139
|
+
* glyph's bitmap is drawn `top` pixels below that and `left` pixels right of the pen.
|
|
140
|
+
*/
|
|
141
|
+
export function getGlyphFontRenderer(store: GlyphStore, fontStack: string): FontRenderer {
|
|
142
|
+
return {
|
|
143
|
+
measure(character?: string) {
|
|
144
|
+
if (character === undefined) {
|
|
145
|
+
// `TextLayer` sets the baseline `(ascent - descent) / 2` from the middle of the line.
|
|
146
|
+
return {advance: 0, width: 0, ascent: 0, descent: 2 * LINE_GLYPH_OFFSET};
|
|
147
|
+
}
|
|
148
|
+
const glyph = getStyleGlyph(store, fontStack, character);
|
|
149
|
+
if (!glyph) {
|
|
150
|
+
return {advance: 0, width: 0, ascent: 0, descent: 0};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
advance: glyph.advance,
|
|
154
|
+
width: glyph.width,
|
|
155
|
+
ascent: glyph.top,
|
|
156
|
+
descent: glyph.height - glyph.top
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
draw(character: string) {
|
|
160
|
+
const glyph = getStyleGlyph(store, fontStack, character);
|
|
161
|
+
if (!glyph || !glyph.bitmap.length) {
|
|
162
|
+
return {data: new ImageData(1, 1), left: 0, top: 0};
|
|
163
|
+
}
|
|
164
|
+
const width = glyph.width + 2 * GLYPH_BORDER;
|
|
165
|
+
const height = glyph.height + 2 * GLYPH_BORDER;
|
|
166
|
+
const data = new ImageData(width, height);
|
|
167
|
+
for (let i = 0; i < glyph.bitmap.length; i++) {
|
|
168
|
+
data.data[4 * i + 3] = glyph.bitmap[i];
|
|
169
|
+
}
|
|
170
|
+
return {data, left: GLYPH_BORDER - glyph.left, top: GLYPH_BORDER};
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
package/src/glyph-pbf.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// tangram-layers
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
// deck.gl-community
|
|
6
|
+
// SPDX-License-Identifier: MIT
|
|
7
|
+
// Copyright (c) vis.gl contributors
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The pixels of distance field around each glyph of a glyph range, on every side, beyond its
|
|
11
|
+
* `width` and `height`: a bitmap is `width + 6` by `height + 6`.
|
|
12
|
+
*/
|
|
13
|
+
export const GLYPH_BORDER = 3;
|
|
14
|
+
|
|
15
|
+
/** The size, in pixels, at which glyph ranges are drawn: one em. */
|
|
16
|
+
export const GLYPH_EM = 24;
|
|
17
|
+
|
|
18
|
+
/** Code points per glyph range: a range file holds `256 * n` to `256 * n + 255`. */
|
|
19
|
+
export const GLYPH_RANGE_SIZE = 256;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A glyph of a glyph range, as MapLibre GL JS reads it: a signed distance field and its metrics,
|
|
23
|
+
* in pixels at {@link GLYPH_EM}.
|
|
24
|
+
*/
|
|
25
|
+
export type Glyph = {
|
|
26
|
+
/** Unicode code point. */
|
|
27
|
+
id: number;
|
|
28
|
+
/**
|
|
29
|
+
* One byte per pixel, `width + 2 * GLYPH_BORDER` by `height + 2 * GLYPH_BORDER`; empty for a
|
|
30
|
+
* glyph without ink, such as a space.
|
|
31
|
+
*/
|
|
32
|
+
bitmap: Uint8Array | Uint8ClampedArray;
|
|
33
|
+
/** Width of the ink. */
|
|
34
|
+
width: number;
|
|
35
|
+
/** Height of the ink. */
|
|
36
|
+
height: number;
|
|
37
|
+
/** From the pen position to the left of the ink. */
|
|
38
|
+
left: number;
|
|
39
|
+
/** Vertical bearing of the top of the ink, as MapLibre GL JS reads it. */
|
|
40
|
+
top: number;
|
|
41
|
+
/** From the pen position to the next. */
|
|
42
|
+
advance: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** A reader of the protocol buffer encoding: just what glyph ranges use. */
|
|
46
|
+
class Reader {
|
|
47
|
+
position = 0;
|
|
48
|
+
constructor(readonly bytes: Uint8Array) {}
|
|
49
|
+
|
|
50
|
+
readVarint(): number {
|
|
51
|
+
let value = 0;
|
|
52
|
+
let shift = 0;
|
|
53
|
+
for (;;) {
|
|
54
|
+
if (this.position >= this.bytes.length) {
|
|
55
|
+
throw new Error('Truncated glyph range');
|
|
56
|
+
}
|
|
57
|
+
const byte = this.bytes[this.position++];
|
|
58
|
+
// Glyph fields fit in 32 bits; multiplying keeps larger values exact, up to 2^53.
|
|
59
|
+
value += (byte & 0x7f) * 2 ** shift;
|
|
60
|
+
if (byte < 0x80) {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
shift += 7;
|
|
64
|
+
if (shift > 49) {
|
|
65
|
+
throw new Error('Invalid varint in glyph range');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
readSVarint(): number {
|
|
71
|
+
const value = this.readVarint();
|
|
72
|
+
return value % 2 === 1 ? -(value + 1) / 2 : value / 2;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The bytes of a length-delimited field, which the reader then skips. */
|
|
76
|
+
readBytes(): Uint8Array {
|
|
77
|
+
const length = this.readVarint();
|
|
78
|
+
const end = this.position + length;
|
|
79
|
+
if (end > this.bytes.length) {
|
|
80
|
+
throw new Error('Truncated glyph range');
|
|
81
|
+
}
|
|
82
|
+
const bytes = this.bytes.subarray(this.position, end);
|
|
83
|
+
this.position = end;
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
skip(wireType: number): void {
|
|
88
|
+
if (wireType === 0) {
|
|
89
|
+
this.readVarint();
|
|
90
|
+
} else if (wireType === 1) {
|
|
91
|
+
this.position += 8;
|
|
92
|
+
} else if (wireType === 2) {
|
|
93
|
+
this.readBytes();
|
|
94
|
+
} else if (wireType === 5) {
|
|
95
|
+
this.position += 4;
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`Unsupported wire type ${wireType} in glyph range`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Calls `read` with the tag and wire type of each field, which `read` must consume or skip. */
|
|
102
|
+
readFields(read: (tag: number, wireType: number) => void): void {
|
|
103
|
+
while (this.position < this.bytes.length) {
|
|
104
|
+
const key = this.readVarint();
|
|
105
|
+
read(Math.floor(key / 8), key % 8);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readGlyph(bytes: Uint8Array): Glyph | null {
|
|
111
|
+
const reader = new Reader(bytes);
|
|
112
|
+
const glyph: Partial<Glyph> = {};
|
|
113
|
+
reader.readFields((tag, wireType) => {
|
|
114
|
+
if (tag === 1 && wireType === 0) glyph.id = reader.readVarint();
|
|
115
|
+
else if (tag === 2 && wireType === 2) glyph.bitmap = reader.readBytes();
|
|
116
|
+
else if (tag === 3 && wireType === 0) glyph.width = reader.readVarint();
|
|
117
|
+
else if (tag === 4 && wireType === 0) glyph.height = reader.readVarint();
|
|
118
|
+
else if (tag === 5 && wireType === 0) glyph.left = reader.readSVarint();
|
|
119
|
+
else if (tag === 6 && wireType === 0) glyph.top = reader.readSVarint();
|
|
120
|
+
else if (tag === 7 && wireType === 0) glyph.advance = reader.readVarint();
|
|
121
|
+
else reader.skip(wireType);
|
|
122
|
+
});
|
|
123
|
+
const {id, width = 0, height = 0, left = 0, top = 0, advance = 0} = glyph;
|
|
124
|
+
if (id === undefined) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
// MapLibre GL JS reads every bitmap as `width + 6` by `height + 6` (its `AlphaImage`) and fails on
|
|
128
|
+
// any other size. A glyph without ink, such as a space, may leave its bitmap out or carry just
|
|
129
|
+
// the border (CARTO's glyphs do): either way it draws nothing, and only its advance counts.
|
|
130
|
+
const inked = width > 0 && height > 0;
|
|
131
|
+
const size = (width + 2 * GLYPH_BORDER) * (height + 2 * GLYPH_BORDER);
|
|
132
|
+
if (glyph.bitmap ? glyph.bitmap.length !== size : inked) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const bitmap = inked ? glyph.bitmap! : new Uint8Array(0);
|
|
136
|
+
return {id, bitmap, width, height, left, top, advance};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reads a glyph range, the `{fontstack}/{range}.pbf` file a style's `glyphs` URL points to: the
|
|
141
|
+
* glyphs of every font stack it holds. Glyphs whose bitmap does not match their size are skipped.
|
|
142
|
+
*/
|
|
143
|
+
export function parseGlyphRange(data: ArrayBuffer | Uint8Array): Glyph[] {
|
|
144
|
+
const glyphs: Glyph[] = [];
|
|
145
|
+
const reader = new Reader(data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
146
|
+
reader.readFields((tag, wireType) => {
|
|
147
|
+
if (tag !== 1 || wireType !== 2) {
|
|
148
|
+
reader.skip(wireType);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const fontstack = new Reader(reader.readBytes());
|
|
152
|
+
fontstack.readFields((fontstackTag, fontstackWireType) => {
|
|
153
|
+
if (fontstackTag === 3 && fontstackWireType === 2) {
|
|
154
|
+
const glyph = readGlyph(fontstack.readBytes());
|
|
155
|
+
if (glyph) {
|
|
156
|
+
glyphs.push(glyph);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
fontstack.skip(fontstackWireType);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
return glyphs;
|
|
164
|
+
}
|