@sakuzu/maplibre-gl-draw 2.1.2 → 2.1.4
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/CHANGELOG.md +55 -0
- package/dist/api/impl/engine.js +6 -0
- package/dist/api/impl/extension-host.js +3 -1
- package/dist/api/impl/input.js +12 -3
- package/dist/dataset/chunk-set.js +2 -3
- package/dist/dataset/dataset.js +6 -4
- package/dist/dataset/retained.js +3 -3
- package/dist/dataset/selection.js +1 -1
- package/dist/dispatcher/input-router.js +5 -3
- package/dist/modes/select/mode.js +8 -0
- package/dist/snapping/service.js +16 -0
- package/dist/view/layer/renderers.d.ts +3 -0
- package/dist/view/layer/renderers.js +6 -1
- package/dist/view/renderers/anchored-outline.d.ts +5 -0
- package/dist/view/renderers/anchored-outline.js +352 -0
- package/dist/view/renderers/batch-manager.js +25 -17
- package/dist/view/renderers/draw-factors.d.ts +11 -1
- package/dist/view/renderers/draw-factors.js +13 -0
- package/dist/view/renderers/line/line-shader.js +6 -1
- package/dist/view/renderers/point/point-instance.d.ts +19 -0
- package/dist/view/renderers/point/point-instance.js +47 -1
- package/dist/view/renderers/polygon/sdf-polygon.js +5 -1
- package/dist/view/shaders/initializer.d.ts +3 -0
- package/dist/view/shaders/initializer.js +8 -2
- package/dist/view/terrain/drape/renderer.js +11 -2
- package/dist/view/ui/selection-ui/renderer.d.ts +2 -1
- package/dist/view/ui/selection-ui/renderer.js +20 -131
- package/package.json +1 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
import { getDashPattern, RENDERING_DEFAULTS } from '../../shared/math/index.js';
|
|
4
|
+
import { resolvePixelRatio } from '../../shared/utils/pixel-ratio.js';
|
|
5
|
+
import { calculateLngLatOffset, createProgram, getProjectionTransitionUniform, OFFSET_MODE_GLSL, } from '../shaders/helpers.js';
|
|
6
|
+
import { ProjectionUniformManager } from '../shaders/projection.js';
|
|
7
|
+
import { TerrainContext } from '../terrain/context.js';
|
|
8
|
+
import { drawBillboardsWithoutDepth } from './point/billboard-depth.js';
|
|
9
|
+
/** Floats per vertex: the offset (x, y in device px), the signed distance across, the distance along */
|
|
10
|
+
const FLOATS_PER_VERTEX = 4;
|
|
11
|
+
const STRIDE = FLOATS_PER_VERTEX * 4;
|
|
12
|
+
/**
|
|
13
|
+
* The vertices of a closed outline as a triangle strip: two per corner (the two sides of the
|
|
14
|
+
* line) and the first corner again at the end
|
|
15
|
+
*
|
|
16
|
+
* Each vertex is the corner moved along the miter of its two edges by half the width plus one
|
|
17
|
+
* pixel (the band the HHAA coverage fades in), the same join as StrokeRenderer: a miter longer
|
|
18
|
+
* than the limit becomes a bevel along the edge that arrives. The distance along the outline is
|
|
19
|
+
* in CSS px, the unit of the dash patterns.
|
|
20
|
+
*
|
|
21
|
+
* @param corners The corners relative to the anchor, in CSS px, in order (not closed)
|
|
22
|
+
* @param widthPx The width of the line in device px
|
|
23
|
+
* @param pixelRatio Device px per CSS px
|
|
24
|
+
* @param miterLimit The longest miter, in half widths
|
|
25
|
+
* @returns The interleaved vertices (x, y, signed distance, distance), empty for fewer than two
|
|
26
|
+
* distinct corners
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export function buildOutlineStrip(corners, widthPx, pixelRatio, miterLimit = RENDERING_DEFAULTS.MITER_LIMIT) {
|
|
30
|
+
// Repeated corners make no edge
|
|
31
|
+
const ring = corners.filter((corner, i) => {
|
|
32
|
+
const next = corners[(i + 1) % corners.length];
|
|
33
|
+
return corners.length === 1 || corner.x !== next.x || corner.y !== next.y;
|
|
34
|
+
});
|
|
35
|
+
const n = ring.length;
|
|
36
|
+
if (n < 2)
|
|
37
|
+
return new Float32Array(0);
|
|
38
|
+
const expanded = widthPx / 2 + 1;
|
|
39
|
+
const unit = (from, to) => {
|
|
40
|
+
const dx = to.x - from.x;
|
|
41
|
+
const dy = to.y - from.y;
|
|
42
|
+
const length = Math.hypot(dx, dy);
|
|
43
|
+
return { x: dx / length, y: dy / length };
|
|
44
|
+
};
|
|
45
|
+
const data = new Float32Array((n + 1) * 2 * FLOATS_PER_VERTEX);
|
|
46
|
+
let along = 0;
|
|
47
|
+
for (let i = 0; i <= n; i++) {
|
|
48
|
+
const corner = ring[i % n];
|
|
49
|
+
if (i > 0) {
|
|
50
|
+
const before = ring[i - 1];
|
|
51
|
+
along += Math.hypot(corner.x - before.x, corner.y - before.y);
|
|
52
|
+
}
|
|
53
|
+
const arriving = unit(ring[(i + n - 1) % n], corner);
|
|
54
|
+
const leaving = unit(corner, ring[(i + 1) % n]);
|
|
55
|
+
const n1 = { x: -arriving.y, y: arriving.x };
|
|
56
|
+
const n2 = { x: -leaving.y, y: leaving.x };
|
|
57
|
+
let normal = n1;
|
|
58
|
+
let scale = 1;
|
|
59
|
+
const sum = { x: n1.x + n2.x, y: n1.y + n2.y };
|
|
60
|
+
const sumLength = Math.hypot(sum.x, sum.y);
|
|
61
|
+
if (sumLength > 0.001) {
|
|
62
|
+
const miter = { x: sum.x / sumLength, y: sum.y / sumLength };
|
|
63
|
+
const cosHalf = miter.x * n1.x + miter.y * n1.y;
|
|
64
|
+
if (cosHalf > 0.001 && 1 / cosHalf <= miterLimit) {
|
|
65
|
+
normal = miter;
|
|
66
|
+
scale = 1 / cosHalf;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const side of [1, -1]) {
|
|
70
|
+
const base = (i * 2 + (side === 1 ? 0 : 1)) * FLOATS_PER_VERTEX;
|
|
71
|
+
data[base] = corner.x * pixelRatio + normal.x * side * expanded * scale;
|
|
72
|
+
data[base + 1] = corner.y * pixelRatio + normal.y * side * expanded * scale;
|
|
73
|
+
data[base + 2] = side * expanded;
|
|
74
|
+
data[base + 3] = along;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Draws closed outlines laid in pixels around an anchor projected like a marker
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
export class AnchoredOutlineRenderer {
|
|
85
|
+
constructor(_map, gl, pixelRatio, terrain = new TerrainContext()) {
|
|
86
|
+
Object.defineProperty(this, "gl", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
writable: true,
|
|
90
|
+
value: void 0
|
|
91
|
+
});
|
|
92
|
+
Object.defineProperty(this, "program", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
writable: true,
|
|
96
|
+
value: null
|
|
97
|
+
});
|
|
98
|
+
Object.defineProperty(this, "variantName", {
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true,
|
|
101
|
+
writable: true,
|
|
102
|
+
value: ''
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(this, "projectionUniformManager", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: void 0
|
|
109
|
+
});
|
|
110
|
+
Object.defineProperty(this, "offsetUniforms", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: null
|
|
115
|
+
});
|
|
116
|
+
/** The injected device pixel ratio (read from window each time when not injected) */
|
|
117
|
+
Object.defineProperty(this, "pixelRatio", {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
writable: true,
|
|
121
|
+
value: void 0
|
|
122
|
+
});
|
|
123
|
+
Object.defineProperty(this, "colorLoc", {
|
|
124
|
+
enumerable: true,
|
|
125
|
+
configurable: true,
|
|
126
|
+
writable: true,
|
|
127
|
+
value: null
|
|
128
|
+
});
|
|
129
|
+
Object.defineProperty(this, "widthLoc", {
|
|
130
|
+
enumerable: true,
|
|
131
|
+
configurable: true,
|
|
132
|
+
writable: true,
|
|
133
|
+
value: null
|
|
134
|
+
});
|
|
135
|
+
Object.defineProperty(this, "pixelToClipLoc", {
|
|
136
|
+
enumerable: true,
|
|
137
|
+
configurable: true,
|
|
138
|
+
writable: true,
|
|
139
|
+
value: null
|
|
140
|
+
});
|
|
141
|
+
Object.defineProperty(this, "anchorOffsetLoc", {
|
|
142
|
+
enumerable: true,
|
|
143
|
+
configurable: true,
|
|
144
|
+
writable: true,
|
|
145
|
+
value: null
|
|
146
|
+
});
|
|
147
|
+
Object.defineProperty(this, "anchorElevationLoc", {
|
|
148
|
+
enumerable: true,
|
|
149
|
+
configurable: true,
|
|
150
|
+
writable: true,
|
|
151
|
+
value: null
|
|
152
|
+
});
|
|
153
|
+
Object.defineProperty(this, "dashArrayLoc", {
|
|
154
|
+
enumerable: true,
|
|
155
|
+
configurable: true,
|
|
156
|
+
writable: true,
|
|
157
|
+
value: null
|
|
158
|
+
});
|
|
159
|
+
Object.defineProperty(this, "dashEnabledLoc", {
|
|
160
|
+
enumerable: true,
|
|
161
|
+
configurable: true,
|
|
162
|
+
writable: true,
|
|
163
|
+
value: null
|
|
164
|
+
});
|
|
165
|
+
Object.defineProperty(this, "vao", {
|
|
166
|
+
enumerable: true,
|
|
167
|
+
configurable: true,
|
|
168
|
+
writable: true,
|
|
169
|
+
value: void 0
|
|
170
|
+
});
|
|
171
|
+
Object.defineProperty(this, "vertexBuffer", {
|
|
172
|
+
enumerable: true,
|
|
173
|
+
configurable: true,
|
|
174
|
+
writable: true,
|
|
175
|
+
value: void 0
|
|
176
|
+
});
|
|
177
|
+
this.gl = gl;
|
|
178
|
+
this.pixelRatio = pixelRatio;
|
|
179
|
+
// An outline around an anchor is a symbol: it keeps the height of its anchor on the frames
|
|
180
|
+
// that lay the surfaces flat, as the marker does
|
|
181
|
+
this.projectionUniformManager = new ProjectionUniformManager(gl).useTerrainState(terrain);
|
|
182
|
+
this.vertexBuffer = gl.createBuffer();
|
|
183
|
+
this.vao = gl.createVertexArray();
|
|
184
|
+
gl.bindVertexArray(this.vao);
|
|
185
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
186
|
+
gl.enableVertexAttribArray(0);
|
|
187
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, STRIDE, 0); // a_offset
|
|
188
|
+
gl.enableVertexAttribArray(1);
|
|
189
|
+
gl.vertexAttribPointer(1, 1, gl.FLOAT, false, STRIDE, 8); // a_side_dist
|
|
190
|
+
gl.enableVertexAttribArray(2);
|
|
191
|
+
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 12); // a_distance
|
|
192
|
+
gl.bindVertexArray(null);
|
|
193
|
+
}
|
|
194
|
+
/** Makes sure the shader of the projection variant exists */
|
|
195
|
+
ensureShader(shaderData) {
|
|
196
|
+
if (this.variantName === shaderData.variantName)
|
|
197
|
+
return;
|
|
198
|
+
if (this.program)
|
|
199
|
+
this.gl.deleteProgram(this.program);
|
|
200
|
+
this.program = this.createShaderProgram(shaderData.vertexShaderPrelude, shaderData.define);
|
|
201
|
+
this.variantName = shaderData.variantName;
|
|
202
|
+
this.projectionUniformManager.getLocations(this.program);
|
|
203
|
+
const gl = this.gl;
|
|
204
|
+
this.colorLoc = gl.getUniformLocation(this.program, 'u_color');
|
|
205
|
+
this.widthLoc = gl.getUniformLocation(this.program, 'u_width');
|
|
206
|
+
this.pixelToClipLoc = gl.getUniformLocation(this.program, 'u_pixel_to_clip');
|
|
207
|
+
this.anchorOffsetLoc = gl.getUniformLocation(this.program, 'u_anchor_offset');
|
|
208
|
+
this.anchorElevationLoc = gl.getUniformLocation(this.program, 'u_anchor_elevation_m');
|
|
209
|
+
this.dashArrayLoc = gl.getUniformLocation(this.program, 'u_dashArray');
|
|
210
|
+
this.dashEnabledLoc = gl.getUniformLocation(this.program, 'u_dashEnabled');
|
|
211
|
+
}
|
|
212
|
+
/** Sets the uniforms of the offset mode (the camera of the copy of the world being drawn) */
|
|
213
|
+
setOffsetUniforms(uniforms) {
|
|
214
|
+
this.offsetUniforms = uniforms;
|
|
215
|
+
}
|
|
216
|
+
createShaderProgram(prelude, define) {
|
|
217
|
+
const projTransitionUniform = getProjectionTransitionUniform(prelude);
|
|
218
|
+
const vertexSource = `#version 300 es
|
|
219
|
+
${prelude}
|
|
220
|
+
${define}
|
|
221
|
+
${projTransitionUniform}${OFFSET_MODE_GLSL}
|
|
222
|
+
layout(location = 0) in vec2 a_offset; // the vertex around the anchor (device px, y down)
|
|
223
|
+
layout(location = 1) in float a_side_dist; // the signed distance across the line (device px)
|
|
224
|
+
layout(location = 2) in float a_distance; // the distance along the outline (CSS px)
|
|
225
|
+
|
|
226
|
+
uniform vec2 u_anchor_offset; // the anchor relative to the center (64-bit on the CPU)
|
|
227
|
+
uniform float u_anchor_elevation_m; // the height of the anchor, from the CPU as for a marker
|
|
228
|
+
uniform vec2 u_pixel_to_clip; // device px to clip space
|
|
229
|
+
uniform float u_width; // the width of the line (device px)
|
|
230
|
+
|
|
231
|
+
out float v_dist;
|
|
232
|
+
out float v_width;
|
|
233
|
+
out float v_distance;
|
|
234
|
+
|
|
235
|
+
void main() {
|
|
236
|
+
// The height comes from the CPU, the source hit testing and the marker use
|
|
237
|
+
g_anchor_elevation_m = u_anchor_elevation_m;
|
|
238
|
+
g_anchor_elevation_on = 1.0;
|
|
239
|
+
vec4 centerClip = project_position_to_clipspace_from_offset(u_anchor_offset, u_projection_matrix);
|
|
240
|
+
// The depth of the anchor for every vertex: the far side of the globe clips the outline
|
|
241
|
+
// with its anchor, as it clips a marker
|
|
242
|
+
gl_Position = vec4(
|
|
243
|
+
centerClip.xy / centerClip.w + vec2(a_offset.x, -a_offset.y) * u_pixel_to_clip,
|
|
244
|
+
centerClip.z / centerClip.w,
|
|
245
|
+
1.0
|
|
246
|
+
);
|
|
247
|
+
v_dist = a_side_dist;
|
|
248
|
+
v_width = u_width / 2.0;
|
|
249
|
+
v_distance = a_distance;
|
|
250
|
+
}`;
|
|
251
|
+
const fragmentSource = `#version 300 es
|
|
252
|
+
precision highp float;
|
|
253
|
+
|
|
254
|
+
uniform vec4 u_color;
|
|
255
|
+
uniform vec4 u_dashArray; // [dashLength, gapLength, patternLength, 0]
|
|
256
|
+
uniform float u_dashEnabled;
|
|
257
|
+
|
|
258
|
+
in float v_dist;
|
|
259
|
+
in float v_width;
|
|
260
|
+
in float v_distance;
|
|
261
|
+
|
|
262
|
+
out vec4 fragColor;
|
|
263
|
+
|
|
264
|
+
// HHAA: the part of the pixel on the inner side of a half plane
|
|
265
|
+
float pixelLine(float x) {
|
|
266
|
+
return clamp(x + 0.5, 0.0, 1.0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void main() {
|
|
270
|
+
if (u_dashEnabled > 0.5) {
|
|
271
|
+
float patternLength = u_dashArray.z;
|
|
272
|
+
if (patternLength > 0.0 && mod(v_distance, patternLength) > u_dashArray.x) discard;
|
|
273
|
+
}
|
|
274
|
+
float coverage = pixelLine(v_dist + v_width) - pixelLine(v_dist - v_width);
|
|
275
|
+
// Blending is non-premultiplied, so the coverage goes to the alpha only
|
|
276
|
+
fragColor = vec4(u_color.rgb, u_color.a * coverage);
|
|
277
|
+
}`;
|
|
278
|
+
return createProgram(this.gl, vertexSource, fragmentSource);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Draws a closed outline around an anchor
|
|
282
|
+
*
|
|
283
|
+
* @param anchor The position the outline is laid around, `[lng, lat]`
|
|
284
|
+
* @param elevationMeters The height of the anchor (the height its marker is drawn at)
|
|
285
|
+
* @param corners The corners relative to the anchor's point on the screen, in CSS px
|
|
286
|
+
* @param style The line (its width is in CSS px)
|
|
287
|
+
* @param opacity A factor on the opacity of the line (the terrain's ghost of the marker)
|
|
288
|
+
* @param zoom The zoom of the frame
|
|
289
|
+
* @param projectionData The projection of the copy of the world being drawn
|
|
290
|
+
*/
|
|
291
|
+
draw(anchor, elevationMeters, corners, style, opacity, zoom, projectionData) {
|
|
292
|
+
if (!this.program)
|
|
293
|
+
return;
|
|
294
|
+
const gl = this.gl;
|
|
295
|
+
const pixelRatio = resolvePixelRatio(this.pixelRatio);
|
|
296
|
+
const widthPx = style.width * pixelRatio;
|
|
297
|
+
const data = buildOutlineStrip(corners, widthPx, pixelRatio);
|
|
298
|
+
if (data.length === 0)
|
|
299
|
+
return;
|
|
300
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
301
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
|
|
302
|
+
gl.bindVertexArray(this.vao);
|
|
303
|
+
gl.useProgram(this.program);
|
|
304
|
+
this.projectionUniformManager.setUniforms(projectionData, zoom, this.offsetUniforms);
|
|
305
|
+
const center = this.offsetUniforms?.centerLngLat ?? [0, 0];
|
|
306
|
+
const offset = calculateLngLatOffset([anchor[0], anchor[1]], center);
|
|
307
|
+
if (this.anchorOffsetLoc)
|
|
308
|
+
gl.uniform2f(this.anchorOffsetLoc, offset[0], offset[1]);
|
|
309
|
+
if (this.anchorElevationLoc)
|
|
310
|
+
gl.uniform1f(this.anchorElevationLoc, elevationMeters);
|
|
311
|
+
if (this.pixelToClipLoc) {
|
|
312
|
+
gl.uniform2f(this.pixelToClipLoc, 2 / gl.drawingBufferWidth, 2 / gl.drawingBufferHeight);
|
|
313
|
+
}
|
|
314
|
+
if (this.widthLoc)
|
|
315
|
+
gl.uniform1f(this.widthLoc, widthPx);
|
|
316
|
+
if (this.colorLoc) {
|
|
317
|
+
gl.uniform4fv(this.colorLoc, [
|
|
318
|
+
style.color[0],
|
|
319
|
+
style.color[1],
|
|
320
|
+
style.color[2],
|
|
321
|
+
style.color[3] * style.opacity * opacity,
|
|
322
|
+
]);
|
|
323
|
+
}
|
|
324
|
+
const dashPattern = style.dashArray ?? getDashPattern(style.lineStyle);
|
|
325
|
+
const dashed = style.lineStyle !== 'solid' && dashPattern.length >= 2;
|
|
326
|
+
if (this.dashEnabledLoc)
|
|
327
|
+
gl.uniform1f(this.dashEnabledLoc, dashed ? 1 : 0);
|
|
328
|
+
if (dashed && this.dashArrayLoc) {
|
|
329
|
+
const [dash, gap] = dashPattern;
|
|
330
|
+
gl.uniform4fv(this.dashArrayLoc, [dash, gap, dash + gap, 0]);
|
|
331
|
+
}
|
|
332
|
+
drawBillboardsWithoutDepth(gl, () => {
|
|
333
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, data.length / FLOATS_PER_VERTEX);
|
|
334
|
+
});
|
|
335
|
+
gl.bindVertexArray(null);
|
|
336
|
+
}
|
|
337
|
+
/** Releases the GPU resources */
|
|
338
|
+
dispose() {
|
|
339
|
+
if (this.program) {
|
|
340
|
+
this.gl.deleteProgram(this.program);
|
|
341
|
+
this.program = null;
|
|
342
|
+
}
|
|
343
|
+
if (this.vao) {
|
|
344
|
+
this.gl.deleteVertexArray(this.vao);
|
|
345
|
+
this.vao = null;
|
|
346
|
+
}
|
|
347
|
+
if (this.vertexBuffer) {
|
|
348
|
+
this.gl.deleteBuffer(this.vertexBuffer);
|
|
349
|
+
this.vertexBuffer = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -6,7 +6,7 @@ import { getCircleRadius, getCreatedZoom } from '../../shared/utils/property.js'
|
|
|
6
6
|
import { densifyPathForGlobe } from '../globe-subdivision.js';
|
|
7
7
|
import { TerrainContext } from '../terrain/context.js';
|
|
8
8
|
import { anchorGhostOpacity } from '../terrain/occlusion.js';
|
|
9
|
-
import { layerDrawFactors, NEUTRAL_DRAW_FACTORS, } from './draw-factors.js';
|
|
9
|
+
import { combineDrawFactors, layerDrawFactors, NEUTRAL_DRAW_FACTORS, } from './draw-factors.js';
|
|
10
10
|
import { getStrokeDashPattern, splitIntoDashes } from './line/dash.js';
|
|
11
11
|
import { toInstancedPointShape, } from './point/point-instance.js';
|
|
12
12
|
/**
|
|
@@ -310,12 +310,15 @@ export class BatchManager {
|
|
|
310
310
|
*
|
|
311
311
|
* @param layer The layer to draw (used to evaluate the style rules, and its opacity is
|
|
312
312
|
* multiplied into everything the frame draws; no factor when omitted)
|
|
313
|
+
* @param factors The draw factors of the frame itself (a dataset passes those of its
|
|
314
|
+
* `zoomScale`). They are combined with the factors of the layer and multiplied into
|
|
315
|
+
* everything the frame draws, the same as on the retained path; no factor when omitted
|
|
313
316
|
*/
|
|
314
|
-
beginFrame(projectionData, zoom, layer) {
|
|
317
|
+
beginFrame(projectionData, zoom, layer, factors = NEUTRAL_DRAW_FACTORS) {
|
|
315
318
|
this.projectionData = projectionData;
|
|
316
319
|
this.zoom = zoom;
|
|
317
320
|
this.layer = layer;
|
|
318
|
-
this.factors = layerDrawFactors(layer);
|
|
321
|
+
this.factors = combineDrawFactors(layerDrawFactors(layer), factors);
|
|
319
322
|
this.currentBatchType = null;
|
|
320
323
|
}
|
|
321
324
|
/**
|
|
@@ -408,7 +411,7 @@ export class BatchManager {
|
|
|
408
411
|
}
|
|
409
412
|
// Shapes that do not support instancing are drawn individually, one point at a time
|
|
410
413
|
// (MultiPoint comes into this function part by part, so they are drawn per point)
|
|
411
|
-
this.featureDrawer.drawPointShape(coord, this.
|
|
414
|
+
this.featureDrawer.drawPointShape(coord, this.withFrameFactors(style), this.zoom, feature.id);
|
|
412
415
|
}
|
|
413
416
|
else {
|
|
414
417
|
// Color, size and stroke are instance attributes, so the batch key only needs the shape
|
|
@@ -474,8 +477,9 @@ export class BatchManager {
|
|
|
474
477
|
// the look does not change).
|
|
475
478
|
const fixedWidth = getCreatedZoom(feature) === undefined;
|
|
476
479
|
const createdZoom = getCreatedZoom(feature) ?? this.zoom;
|
|
477
|
-
// Compute the effective line width at the current zoom level
|
|
478
|
-
|
|
480
|
+
// Compute the effective line width at the current zoom level (with the size factor of the
|
|
481
|
+
// frame, which the shader multiplies into the width, so the dashes follow the drawn width)
|
|
482
|
+
const effectiveStrokeWidth = strokeStyle.width * 2 ** (this.zoom - createdZoom) * this.factors.scale;
|
|
479
483
|
// For dashed/dotted lines the dashes are computed on the CPU side
|
|
480
484
|
const dashPattern = getStrokeDashPattern(strokeStyle.lineStyle, effectiveStrokeWidth);
|
|
481
485
|
if (dashPattern) {
|
|
@@ -588,7 +592,8 @@ export class BatchManager {
|
|
|
588
592
|
// convention as withFixedWidth; collectPolygons in retained mode does the same).
|
|
589
593
|
const fixedWidth = getCreatedZoom(feature) === undefined;
|
|
590
594
|
const createdZoom = getCreatedZoom(feature) ?? this.zoom;
|
|
591
|
-
|
|
595
|
+
// The drawn width of the outline (with the size factor of the frame; the dashes follow it)
|
|
596
|
+
const effectiveStrokeWidth = strokeStyle.width * 2 ** (this.zoom - createdZoom) * this.factors.scale;
|
|
592
597
|
const isDashed = strokeStyle.lineStyle !== 'solid';
|
|
593
598
|
if (strokeStyle.opacity > 0 && !isDashed) {
|
|
594
599
|
// For a solid line: drawn all at once by SDFPolygonRenderer
|
|
@@ -618,7 +623,7 @@ export class BatchManager {
|
|
|
618
623
|
if (fillColor[3] > 0) {
|
|
619
624
|
this.polygonFillBatch.push({
|
|
620
625
|
coordinates: rings,
|
|
621
|
-
color: this.
|
|
626
|
+
color: this.colorWithFrameOpacity(fillColor),
|
|
622
627
|
featureId: feature.id,
|
|
623
628
|
partIndex,
|
|
624
629
|
});
|
|
@@ -634,7 +639,7 @@ export class BatchManager {
|
|
|
634
639
|
if (fillColor[3] > 0) {
|
|
635
640
|
this.polygonFillBatch.push({
|
|
636
641
|
coordinates: rings,
|
|
637
|
-
color: this.
|
|
642
|
+
color: this.colorWithFrameOpacity(fillColor),
|
|
638
643
|
featureId,
|
|
639
644
|
partIndex,
|
|
640
645
|
});
|
|
@@ -678,23 +683,26 @@ export class BatchManager {
|
|
|
678
683
|
}
|
|
679
684
|
}
|
|
680
685
|
/**
|
|
681
|
-
* A color with the opacity of the
|
|
682
|
-
* batch, whose colors are per vertex and which has no factor uniform)
|
|
686
|
+
* A color with the opacity factor of the frame multiplied into its alpha (for the fill-only
|
|
687
|
+
* polygon batch, whose colors are per vertex and which has no factor uniform)
|
|
683
688
|
*/
|
|
684
|
-
|
|
689
|
+
colorWithFrameOpacity(color) {
|
|
685
690
|
const opacity = this.factors.opacity;
|
|
686
691
|
return opacity === 1 ? color : [color[0], color[1], color[2], color[3] * opacity];
|
|
687
692
|
}
|
|
688
693
|
/**
|
|
689
|
-
* A point style with the
|
|
690
|
-
* has no factor uniform)
|
|
694
|
+
* A point style with the factors of the frame multiplied in (for the per-point renderer, which
|
|
695
|
+
* has no factor uniform): the size and the stroke width by the size factor, the opacities by
|
|
696
|
+
* the opacity factor
|
|
691
697
|
*/
|
|
692
|
-
|
|
693
|
-
const opacity = this.factors
|
|
694
|
-
if (opacity === 1)
|
|
698
|
+
withFrameFactors(style) {
|
|
699
|
+
const { scale, opacity } = this.factors;
|
|
700
|
+
if (scale === 1 && opacity === 1)
|
|
695
701
|
return style;
|
|
696
702
|
return {
|
|
697
703
|
...style,
|
|
704
|
+
size: style.size * scale,
|
|
705
|
+
strokeWidth: style.strokeWidth * scale,
|
|
698
706
|
fillOpacity: style.fillOpacity * opacity,
|
|
699
707
|
strokeOpacity: style.strokeOpacity * opacity,
|
|
700
708
|
};
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* Conventions:
|
|
11
11
|
*
|
|
12
12
|
* - The default is `NEUTRAL_DRAW_FACTORS` (1x scale, opacity 1), so calls that
|
|
13
|
-
* pass no factors (Store rendering,
|
|
13
|
+
* pass no factors (the Store rendering, for example) look exactly as before,
|
|
14
14
|
* down to the pixel
|
|
15
|
+
* - The immediate-mode frame of a dataset takes its factors through
|
|
16
|
+
* `BatchManager.beginFrame`, which combines them with the factors of the layer
|
|
17
|
+
* (`combineDrawFactors`)
|
|
15
18
|
* - A uniform is per-program residual state, so even rendering paths that do
|
|
16
19
|
* not use the factors must write 1 (a value left over from a previous draw
|
|
17
20
|
* makes another draw come out thinner; this is the same family of accident as
|
|
@@ -58,3 +61,10 @@ export declare function sanitizeDrawFactors(factors: {
|
|
|
58
61
|
export declare function layerDrawFactors(layer: {
|
|
59
62
|
readonly opacity?: number;
|
|
60
63
|
} | null | undefined): RetainedDrawFactors;
|
|
64
|
+
/**
|
|
65
|
+
* The product of two sets of factors (the frame's own factors over those of the layer)
|
|
66
|
+
*
|
|
67
|
+
* Both are taken as already rounded (`sanitizeDrawFactors`, `layerDrawFactors`). Neutral
|
|
68
|
+
* factors on either side give the other set itself.
|
|
69
|
+
*/
|
|
70
|
+
export declare function combineDrawFactors(a: RetainedDrawFactors, b: RetainedDrawFactors): RetainedDrawFactors;
|
|
@@ -33,3 +33,16 @@ export function layerDrawFactors(layer) {
|
|
|
33
33
|
return NEUTRAL_DRAW_FACTORS;
|
|
34
34
|
return sanitizeDrawFactors({ scale: 1, opacity });
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The product of two sets of factors (the frame's own factors over those of the layer)
|
|
38
|
+
*
|
|
39
|
+
* Both are taken as already rounded (`sanitizeDrawFactors`, `layerDrawFactors`). Neutral
|
|
40
|
+
* factors on either side give the other set itself.
|
|
41
|
+
*/
|
|
42
|
+
export function combineDrawFactors(a, b) {
|
|
43
|
+
if (b === NEUTRAL_DRAW_FACTORS || (b.scale === 1 && b.opacity === 1))
|
|
44
|
+
return a;
|
|
45
|
+
if (a === NEUTRAL_DRAW_FACTORS || (a.scale === 1 && a.opacity === 1))
|
|
46
|
+
return b;
|
|
47
|
+
return { scale: a.scale * b.scale, opacity: a.opacity * b.opacity };
|
|
48
|
+
}
|
|
@@ -254,6 +254,11 @@ void main() {
|
|
|
254
254
|
float lineWidth = (strokeWidth > 0.0
|
|
255
255
|
? strokeWidth * pow(2.0, u_zoom - createdZoom)
|
|
256
256
|
: (strokeWidth < 0.0 ? -strokeWidth : u_width)) * u_size_scale;
|
|
257
|
+
// Below one pixel the line is drawn one pixel wide and fainter by its width, so the ink per
|
|
258
|
+
// unit of length follows the width and goes to nothing with it (the caps of every dash
|
|
259
|
+
// included, which would otherwise keep a blob of a fixed size)
|
|
260
|
+
float ink = clamp(lineWidth, 0.0, 1.0);
|
|
261
|
+
lineWidth = max(lineWidth, 1.0);
|
|
257
262
|
float halfWidth = lineWidth / 2.0;
|
|
258
263
|
|
|
259
264
|
// HHAA: expand by w+1 so that partially covered pixels are included too
|
|
@@ -298,7 +303,7 @@ void main() {
|
|
|
298
303
|
v_linesofar = segmentStart + t * screenSegmentLength; // cumulative distance (for dashes)
|
|
299
304
|
v_line_pos = linePos; // position along the line
|
|
300
305
|
v_seg_length = screenSegmentLength; // segment length (computed on the GPU)
|
|
301
|
-
v_color = a_color;
|
|
306
|
+
v_color = a_color * ink; // line color (instance attribute), by the ink
|
|
302
307
|
}`;
|
|
303
308
|
}
|
|
304
309
|
/**
|
|
@@ -55,6 +55,15 @@ export interface PointInstanceData {
|
|
|
55
55
|
* Cutting the billboard exactly at the outer radius loses the outer half of the antialiasing.
|
|
56
56
|
*/
|
|
57
57
|
export declare const POINT_AA_PADDING_PX = 1;
|
|
58
|
+
/**
|
|
59
|
+
* The smallest outer radius a point is drawn at (device pixels): a diameter of one pixel
|
|
60
|
+
*
|
|
61
|
+
* A smaller point cannot be drawn smaller, since the antialiasing band alone is a pixel wide: its
|
|
62
|
+
* coverage would tend to a blob of a fixed size as the radius goes to 0, and the point would never
|
|
63
|
+
* fade. Below this radius the shape is drawn at it, fainter by the ratio of the areas
|
|
64
|
+
* (`pointInkFactor`), so the ink it leaves follows its area and goes to nothing with it.
|
|
65
|
+
*/
|
|
66
|
+
export declare const POINT_MIN_OUTER_PX = 0.5;
|
|
58
67
|
/**
|
|
59
68
|
* SDF boundaries of one instance (device pixels)
|
|
60
69
|
*/
|
|
@@ -85,6 +94,16 @@ export interface PointSdfEdges {
|
|
|
85
94
|
* @param sizeScale Size factor at draw time (default 1)
|
|
86
95
|
*/
|
|
87
96
|
export declare function pointSdfEdges(fillSizePx: number, strokeWidthPx: number, sizeScale?: number): PointSdfEdges;
|
|
97
|
+
/**
|
|
98
|
+
* The factor the alpha of a point is multiplied by (the same computation as the shader)
|
|
99
|
+
*
|
|
100
|
+
* 1 at an outer radius of `POINT_MIN_OUTER_PX` and above. Below it the point is drawn at that
|
|
101
|
+
* radius (`pointSdfEdges`), and the factor is the ratio of its true area to the area drawn, so the
|
|
102
|
+
* ink it leaves follows its area and goes to 0 with it. A radius of 0 or below draws nothing (0).
|
|
103
|
+
*
|
|
104
|
+
* @param outerPx The outer radius (radius + stroke width, with the size factor; device pixels)
|
|
105
|
+
*/
|
|
106
|
+
export declare function pointInkFactor(outerPx: number): number;
|
|
88
107
|
/**
|
|
89
108
|
* Point batch of the retained mode (GPU resources)
|
|
90
109
|
*
|
|
@@ -34,6 +34,15 @@ const AA_HALF_WIDTH_PX = 0.5;
|
|
|
34
34
|
* Cutting the billboard exactly at the outer radius loses the outer half of the antialiasing.
|
|
35
35
|
*/
|
|
36
36
|
export const POINT_AA_PADDING_PX = 1;
|
|
37
|
+
/**
|
|
38
|
+
* The smallest outer radius a point is drawn at (device pixels): a diameter of one pixel
|
|
39
|
+
*
|
|
40
|
+
* A smaller point cannot be drawn smaller, since the antialiasing band alone is a pixel wide: its
|
|
41
|
+
* coverage would tend to a blob of a fixed size as the radius goes to 0, and the point would never
|
|
42
|
+
* fade. Below this radius the shape is drawn at it, fainter by the ratio of the areas
|
|
43
|
+
* (`pointInkFactor`), so the ink it leaves follows its area and goes to nothing with it.
|
|
44
|
+
*/
|
|
45
|
+
export const POINT_MIN_OUTER_PX = 0.5;
|
|
37
46
|
// Stride of the instance data (floats per instance)
|
|
38
47
|
// position(2) + fillColor(4) + strokeColor(4) + size(1) + strokeWidth(1)
|
|
39
48
|
// + elevation(1) = 13 floats
|
|
@@ -58,8 +67,34 @@ const SHAPE_UNIFORM = POINT_SHAPE_CODE;
|
|
|
58
67
|
export function pointSdfEdges(fillSizePx, strokeWidthPx, sizeScale = 1) {
|
|
59
68
|
const radius = Math.max(0, fillSizePx) * sizeScale;
|
|
60
69
|
const outer = radius + Math.max(0, strokeWidthPx) * sizeScale;
|
|
70
|
+
// Below a diameter of one pixel the shape is drawn at that diameter (pointInkFactor)
|
|
71
|
+
if (outer > 0 && outer < POINT_MIN_OUTER_PX) {
|
|
72
|
+
const grow = POINT_MIN_OUTER_PX / outer;
|
|
73
|
+
return {
|
|
74
|
+
radius: radius * grow,
|
|
75
|
+
outer: POINT_MIN_OUTER_PX,
|
|
76
|
+
extent: POINT_MIN_OUTER_PX + POINT_AA_PADDING_PX,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
61
79
|
return { radius, outer, extent: outer + POINT_AA_PADDING_PX };
|
|
62
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The factor the alpha of a point is multiplied by (the same computation as the shader)
|
|
83
|
+
*
|
|
84
|
+
* 1 at an outer radius of `POINT_MIN_OUTER_PX` and above. Below it the point is drawn at that
|
|
85
|
+
* radius (`pointSdfEdges`), and the factor is the ratio of its true area to the area drawn, so the
|
|
86
|
+
* ink it leaves follows its area and goes to 0 with it. A radius of 0 or below draws nothing (0).
|
|
87
|
+
*
|
|
88
|
+
* @param outerPx The outer radius (radius + stroke width, with the size factor; device pixels)
|
|
89
|
+
*/
|
|
90
|
+
export function pointInkFactor(outerPx) {
|
|
91
|
+
if (!(outerPx > 0))
|
|
92
|
+
return 0;
|
|
93
|
+
if (outerPx >= POINT_MIN_OUTER_PX)
|
|
94
|
+
return 1;
|
|
95
|
+
const ratio = outerPx / POINT_MIN_OUTER_PX;
|
|
96
|
+
return ratio * ratio;
|
|
97
|
+
}
|
|
63
98
|
/**
|
|
64
99
|
* Rounds the origin of the relative coordinates to Float32
|
|
65
100
|
*/
|
|
@@ -326,6 +361,7 @@ flat out vec4 v_stroke_color;
|
|
|
326
361
|
out vec2 v_offset_px;
|
|
327
362
|
flat out float v_radius_px;
|
|
328
363
|
flat out float v_outer_px;
|
|
364
|
+
flat out float v_ink;
|
|
329
365
|
|
|
330
366
|
void main() {
|
|
331
367
|
// The anchor elevation comes from the CPU (the same source as hit testing).
|
|
@@ -338,6 +374,15 @@ void main() {
|
|
|
338
374
|
|
|
339
375
|
float radius = max(a_instance_size, 0.0) * u_size_scale;
|
|
340
376
|
float outer = radius + max(a_instance_stroke_width, 0.0) * u_size_scale;
|
|
377
|
+
// Below a diameter of one pixel the shape is drawn at that diameter, fainter by the ratio of
|
|
378
|
+
// the areas, so the ink it leaves follows its area (pointSdfEdges, pointInkFactor)
|
|
379
|
+
v_ink = 1.0;
|
|
380
|
+
if (outer > 0.0 && outer < ${POINT_MIN_OUTER_PX.toFixed(1)}) {
|
|
381
|
+
float grow = ${POINT_MIN_OUTER_PX.toFixed(1)} / outer;
|
|
382
|
+
v_ink = 1.0 / (grow * grow);
|
|
383
|
+
radius *= grow;
|
|
384
|
+
outer = ${POINT_MIN_OUTER_PX.toFixed(1)};
|
|
385
|
+
}
|
|
341
386
|
// The billboard is sized as the outer radius plus the antialiasing padding
|
|
342
387
|
// (so that the edge is not clipped)
|
|
343
388
|
vec2 offsetPx = a_quad * (outer + ${POINT_AA_PADDING_PX.toFixed(1)});
|
|
@@ -357,6 +402,7 @@ flat in vec4 v_stroke_color;
|
|
|
357
402
|
in vec2 v_offset_px;
|
|
358
403
|
flat in float v_radius_px;
|
|
359
404
|
flat in float v_outer_px;
|
|
405
|
+
flat in float v_ink; // the factor of a point below one pixel (1 otherwise)
|
|
360
406
|
uniform float u_opacity; // opacity factor at draw time (default 1)
|
|
361
407
|
uniform float u_shape; // shape (0 = circle, 1 = square, 2 = triangle, 3 = star)
|
|
362
408
|
out vec4 fragColor;
|
|
@@ -384,7 +430,7 @@ void main() {
|
|
|
384
430
|
vec4 color = mix(v_stroke_color, v_fill_color, fillMix);
|
|
385
431
|
|
|
386
432
|
// Blending is non-premultiplied, so the factors are applied to the alpha only
|
|
387
|
-
fragColor = vec4(color.rgb, color.a * u_opacity * coverage);
|
|
433
|
+
fragColor = vec4(color.rgb, color.a * u_opacity * coverage * v_ink);
|
|
388
434
|
}`;
|
|
389
435
|
return createProgram(this.gl, vertexSource, fragmentSource);
|
|
390
436
|
}
|
|
@@ -376,6 +376,10 @@ void main() {
|
|
|
376
376
|
float lineWidth = (strokeWidth >= 0.0
|
|
377
377
|
? strokeWidth * pow(2.0, u_zoom - createdZoom)
|
|
378
378
|
: -strokeWidth) * u_size_scale;
|
|
379
|
+
// Below one pixel the outline is drawn one pixel wide and fainter by its width, so the
|
|
380
|
+
// ink per unit of length follows the width and goes to nothing with it
|
|
381
|
+
float ink = clamp(lineWidth, 0.0, 1.0);
|
|
382
|
+
lineWidth = max(lineWidth, 1.0);
|
|
379
383
|
float halfWidth = lineWidth / 2.0;
|
|
380
384
|
|
|
381
385
|
// Expand by 1 pixel for HHAA
|
|
@@ -389,7 +393,7 @@ void main() {
|
|
|
389
393
|
vec2 finalNdc = (finalScreenPos / u_viewport) * 2.0 - 1.0;
|
|
390
394
|
gl_Position = vec4(finalNdc * clipPos.w, clipPos.z, clipPos.w);
|
|
391
395
|
|
|
392
|
-
v_color = a_color;
|
|
396
|
+
v_color = vec4(a_color.rgb, a_color.a * ink);
|
|
393
397
|
v_type = 1.0;
|
|
394
398
|
v_dist = a_side * expandedHalfWidth;
|
|
395
399
|
v_halfWidth = halfWidth;
|