mapbox-gl
Version:
A WebGL interactive maps library
156 lines (125 loc) • 4.86 kB
JavaScript
// @flow
import { warnOnce } from '../util/util';
import type Context from '../gl/context';
/**
* A LineAtlas lets us reuse rendered dashed lines
* by writing many of them to a texture and then fetching their positions
* using .getDash.
*
* @param {number} width
* @param {number} height
* @private
*/
class LineAtlas {
width: number;
height: number;
nextRow: number;
bytes: number;
data: Uint8Array;
positions: {[string]: any};
dirty: boolean;
texture: WebGLTexture;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
this.nextRow = 0;
this.bytes = 4;
this.data = new Uint8Array(this.width * this.height * this.bytes);
this.positions = {};
}
/**
* Get or create a dash line pattern.
*
* @param {Array<number>} dasharray
* @param {boolean} round whether to add circle caps in between dash segments
* @returns {Object} position of dash texture in { y, height, width }
* @private
*/
getDash(dasharray: Array<number>, round: boolean) {
const key = dasharray.join(",") + String(round);
if (!this.positions[key]) {
this.positions[key] = this.addDash(dasharray, round);
}
return this.positions[key];
}
addDash(dasharray: Array<number>, round: boolean) {
const n = round ? 7 : 0;
const height = 2 * n + 1;
const offset = 128;
if (this.nextRow + height > this.height) {
warnOnce('LineAtlas out of space');
return null;
}
let length = 0;
for (let i = 0; i < dasharray.length; i++) {
length += dasharray[i];
}
const stretch = this.width / length;
const halfWidth = stretch / 2;
// If dasharray has an odd length, both the first and last parts
// are dashes and should be joined seamlessly.
const oddLength = dasharray.length % 2 === 1;
for (let y = -n; y <= n; y++) {
const row = this.nextRow + n + y;
const index = this.width * row;
let left = oddLength ? -dasharray[dasharray.length - 1] : 0;
let right = dasharray[0];
let partIndex = 1;
for (let x = 0; x < this.width; x++) {
while (right < x / stretch) {
left = right;
right = right + dasharray[partIndex];
if (oddLength && partIndex === dasharray.length - 1) {
right += dasharray[0];
}
partIndex++;
}
const distLeft = Math.abs(x - left * stretch);
const distRight = Math.abs(x - right * stretch);
const dist = Math.min(distLeft, distRight);
const inside = (partIndex % 2) === 1;
let signedDistance;
if (round) {
// Add circle caps
const distMiddle = n ? y / n * (halfWidth + 1) : 0;
if (inside) {
const distEdge = halfWidth - Math.abs(distMiddle);
signedDistance = Math.sqrt(dist * dist + distEdge * distEdge);
} else {
signedDistance = halfWidth - Math.sqrt(dist * dist + distMiddle * distMiddle);
}
} else {
signedDistance = (inside ? 1 : -1) * dist;
}
this.data[3 + (index + x) * 4] = Math.max(0, Math.min(255, signedDistance + offset));
}
}
const pos = {
y: (this.nextRow + n + 0.5) / this.height,
height: 2 * n / this.height,
width: length
};
this.nextRow += height;
this.dirty = true;
return pos;
}
bind(context: Context) {
const gl = context.gl;
if (!this.texture) {
this.texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.data);
} else {
gl.bindTexture(gl.TEXTURE_2D, this.texture);
if (this.dirty) {
this.dirty = false;
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, this.height, gl.RGBA, gl.UNSIGNED_BYTE, this.data);
}
}
}
}
export default LineAtlas;