Spaces:
Sleeping
Sleeping
| ; | |
| 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); | |
| } | |
| } | |