kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
193 lines (159 loc) • 7.16 kB
JavaScript
'use strict';
module.exports = TinySDF;
module.exports.default = TinySDF;
var INF = 1e20;
function TinySDF(fontSize, buffer, radius, cutoff, fontFamily, fontWeight) {
this.fontSize = fontSize || 24;
this.buffer = buffer === undefined ? 3 : buffer;
this.cutoff = cutoff || 0.25;
this.fontFamily = fontFamily || 'sans-serif';
this.fontWeight = fontWeight || 'normal';
this.radius = radius || 8;
// For backwards compatibility, we honor the implicit contract that the
// size of the returned bitmap will be fontSize + buffer * 2
var size = this.size = this.fontSize + this.buffer * 2;
// Glyphs may be slightly larger than their fontSize. The canvas already
// has buffer space, but create extra buffer space in the output grid for the
// "halo" to extend into (if metric extraction is enabled)
var gridSize = size + this.buffer * 2;
this.canvas = document.createElement('canvas');
this.canvas.width = this.canvas.height = size;
this.ctx = this.canvas.getContext('2d');
this.ctx.font = this.fontWeight + ' ' + this.fontSize + 'px ' + this.fontFamily;
this.ctx.textAlign = 'left'; // Necessary so that RTL text doesn't have different alignment
this.ctx.fillStyle = 'black';
// temporary arrays for the distance transform
this.gridOuter = new Float64Array(gridSize * gridSize);
this.gridInner = new Float64Array(gridSize * gridSize);
this.f = new Float64Array(gridSize);
this.z = new Float64Array(gridSize + 1);
this.v = new Uint16Array(gridSize);
this.useMetrics = this.ctx.measureText('A').actualBoundingBoxLeft !== undefined;
// hack around https://bugzilla.mozilla.org/show_bug.cgi?id=737852
this.middle = Math.round((size / 2) * (navigator.userAgent.indexOf('Gecko/') >= 0 ? 1.2 : 1));
}
function prepareGrids(imgData, width, height, glyphWidth, glyphHeight, gridOuter, gridInner) {
// Initialize grids outside the glyph range to alpha 0
gridOuter.fill(INF, 0, width * height);
gridInner.fill(0, 0, width * height);
var offset = (width - glyphWidth) / 2; // This is zero if we're not extracting metrics
for (var y = 0; y < glyphHeight; y++) {
for (var x = 0; x < glyphWidth; x++) {
var j = (y + offset) * width + x + offset;
var a = imgData.data[4 * (y * glyphWidth + x) + 3] / 255; // alpha value
if (a === 1) {
gridOuter[j] = 0;
gridInner[j] = INF;
} else if (a === 0) {
gridOuter[j] = INF;
gridInner[j] = 0;
} else {
var b = Math.max(0, 0.5 - a);
var c = Math.max(0, a - 0.5);
gridOuter[j] = b * b;
gridInner[j] = c * c;
}
}
}
}
function extractAlpha(alphaChannel, width, height, gridOuter, gridInner, radius, cutoff) {
for (var i = 0; i < width * height; i++) {
var d = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]);
alphaChannel[i] = Math.round(255 - 255 * (d / radius + cutoff));
}
}
TinySDF.prototype._draw = function (char, getMetrics) {
var textMetrics = this.ctx.measureText(char);
// Older browsers only expose the glyph width
// This is enough for basic layout with all glyphs using the same fixed size
var advance = textMetrics.width;
var doubleBuffer = 2 * this.buffer;
var width, glyphWidth, height, glyphHeight, top;
var imgTop, imgLeft, baselinePosition;
// If the browser supports bounding box metrics, we can generate a smaller
// SDF. This is a significant performance win.
if (getMetrics && this.useMetrics) {
// The integer/pixel part of the top alignment is encoded in metrics.top
// The remainder is implicitly encoded in the rasterization
top = Math.floor(textMetrics.actualBoundingBoxAscent);
baselinePosition = this.buffer + Math.ceil(textMetrics.actualBoundingBoxAscent);
imgTop = this.buffer;
imgLeft = this.buffer;
// If the glyph overflows the canvas size, it will be clipped at the
// bottom/right
glyphWidth = Math.min(this.size,
Math.ceil(textMetrics.actualBoundingBoxRight - textMetrics.actualBoundingBoxLeft));
glyphHeight = Math.min(this.size - imgTop,
Math.ceil(textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent));
width = glyphWidth + doubleBuffer;
height = glyphHeight + doubleBuffer;
this.ctx.textBaseline = 'alphabetic';
} else {
width = glyphWidth = this.size;
height = glyphHeight = this.size;
// 19 points is an approximation of the "cap height" ascent from alphabetic
// baseline (even though actual drawing is from middle baseline, we can
// use the approximation because every glyph fills the em box)
top = 19 * this.fontSize / 24;
imgTop = imgLeft = 0;
baselinePosition = this.middle;
this.ctx.textBaseline = 'middle';
}
var imgData;
if (glyphWidth && glyphHeight) {
this.ctx.clearRect(imgLeft, imgTop, glyphWidth, glyphHeight);
this.ctx.fillText(char, this.buffer, baselinePosition);
imgData = this.ctx.getImageData(imgLeft, imgTop, glyphWidth, glyphHeight);
}
var alphaChannel = new Uint8ClampedArray(width * height);
prepareGrids(imgData, width, height, glyphWidth, glyphHeight, this.gridOuter, this.gridInner);
edt(this.gridOuter, width, height, this.f, this.v, this.z);
edt(this.gridInner, width, height, this.f, this.v, this.z);
extractAlpha(alphaChannel, width, height, this.gridOuter, this.gridInner, this.radius, this.cutoff);
return {
data: alphaChannel,
metrics: {
width: glyphWidth,
height: glyphHeight,
sdfWidth: width,
sdfHeight: height,
top: top,
left: 0,
advance: advance
}
};
};
TinySDF.prototype.draw = function (char) {
return this._draw(char, false).data;
};
TinySDF.prototype.drawWithMetrics = function (char) {
return this._draw(char, true);
};
// 2D Euclidean squared distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/papers/dt-final.pdf
function edt(data, width, height, f, v, z) {
for (var x = 0; x < width; x++) edt1d(data, x, width, height, f, v, z);
for (var y = 0; y < height; y++) edt1d(data, y * width, 1, width, f, v, z);
}
// 1D squared distance transform
function edt1d(grid, offset, stride, length, f, v, z) {
var q, k, s, r;
v[0] = 0;
z[0] = -INF;
z[1] = INF;
for (q = 0; q < length; q++) f[q] = grid[offset + q * stride];
for (q = 1, k = 0, s = 0; q < length; q++) {
do {
r = v[k];
s = (f[q] - f[r] + q * q - r * r) / (q - r) / 2;
} while (s <= z[k] && --k > -1);
k++;
v[k] = q;
z[k] = s;
z[k + 1] = INF;
}
for (q = 0, k = 0; q < length; q++) {
while (z[k + 1] < q) k++;
r = v[k];
grid[offset + q * stride] = f[r] + (q - r) * (q - r);
}
}