Skip to content

Commit

Permalink
Add drawWithMetrics method that clips SDF to fit browser-provided T…
Browse files Browse the repository at this point in the history
…extMetrics.
  • Loading branch information
ChrisLoer committed Jan 11, 2021
1 parent ca21a1f commit 7d4b379
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 11 deletions.
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2016-2017 Mapbox, Inc.
Copyright © 2016-2021 Mapbox, Inc.
This code available under the terms of the BSD 2-Clause license.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ from system fonts on the browser using Canvas 2D and
[Felzenszwalb/Huttenlocher distance transform](https://cs.brown.edu/~pff/papers/dt-final.pdf).
This is very useful for [rendering text with WebGL](https://www.mapbox.com/blog/text-signed-distance-fields/).

This implementation is based directly on the algorithm published in the Felzenszwalb/Huttenlocher paper, and is not a port of the existing C++ implementation provided by the paper's authors.
This implementation is based directly on the algorithm published in the Felzenszwalb/Huttenlocher paper, and is not a port of the existing C++ implementation provided by the paper's authors.

Demo: http://mapbox.github.io/tiny-sdf/

Expand All @@ -24,4 +24,10 @@ var tinySDFGenerator = new TinySDF(fontsize, buffer, radius, cutoff, fontFamily,

var oneSDF = tinySDFGenerator.draw('');
// returns a Uint8ClampedArray array of alpha values (0–255) for a size x size square grid

// To generate glyphs with variable advances (e.g. non-ideographic glyphs),
// use `drawWithMetrics`
var sdfWithMetrics = tinySDFGenerator.drawWithMetrics('A');
// sdfWithMetrics.data is the same as in `draw`, except the size may be clipped to fit the glyph
// sdfWithMetrics.metrics contains { top, left, width, height, advance } for the glyph
```
52 changes: 44 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,64 @@ function TinySDF(fontSize, buffer, radius, cutoff, fontFamily, fontWeight) {
this.middle = Math.round((size / 2) * (navigator.userAgent.indexOf('Gecko/') >= 0 ? 1.2 : 1));
}

TinySDF.prototype.draw = function (char) {
TinySDF.prototype._draw = function (char, getMetrics) {
this.ctx.clearRect(0, 0, this.size, this.size);
this.ctx.fillText(char, this.buffer, this.middle);
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 metrics = {
width: this.size,
height: this.size,
left: 0,
top: -this.size / 3,
advance: textMetrics.width
};

var imgData;
// If the browser supports bounding box metrics, we can generate a smaller
// SDF. This is a significant performance win.
if (getMetrics && textMetrics.actualBoundingBoxLeft !== undefined) {
metrics.width = Math.ceil(textMetrics.actualBoundingBoxRight - textMetrics.actualBoundingBoxLeft);
metrics.height = Math.ceil(textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent);
metrics.top = Math.floor(textMetrics.actualBoundingBoxAscent) - this.middle;

// The integer/pixel part of the top alignment is encoded in metrics.top
// The remainder is implicitly encoded in the rasterization
var imgTop = this.middle - Math.ceil(textMetrics.actualBoundingBoxAscent) - this.buffer;
imgData = this.ctx.getImageData(0, imgTop, metrics.width + 2 * this.buffer, metrics.height + 2 * this.buffer);
} else {
imgData = this.ctx.getImageData(0, 0, this.size, this.size);
}

var imgData = this.ctx.getImageData(0, 0, this.size, this.size);
var alphaChannel = new Uint8ClampedArray(this.size * this.size);
var alphaChannel = new Uint8ClampedArray(imgData.width * imgData.height);

for (var i = 0; i < this.size * this.size; i++) {
for (var i = 0; i < imgData.width * imgData.height; i++) {
var a = imgData.data[i * 4 + 3] / 255; // alpha value
this.gridOuter[i] = a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2);
this.gridInner[i] = a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2);
}

edt(this.gridOuter, this.size, this.size, this.f, this.v, this.z);
edt(this.gridInner, this.size, this.size, this.f, this.v, this.z);
edt(this.gridOuter, imgData.width, imgData.height, this.f, this.v, this.z);
edt(this.gridInner, imgData.width, imgData.height, this.f, this.v, this.z);

for (i = 0; i < this.size * this.size; i++) {
for (i = 0; i < imgData.width * imgData.height; i++) {
var d = Math.sqrt(this.gridOuter[i]) - Math.sqrt(this.gridInner[i]);
alphaChannel[i] = Math.round(255 - 255 * (d / this.radius + this.cutoff));
}

return alphaChannel;
return {
data: alphaChannel,
metrics: metrics
};
};

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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mapbox/tiny-sdf",
"version": "1.1.0",
"version": "1.2.0",
"description": "Browser-side SDF font generator",
"main": "index.js",
"scripts": {
Expand Down

0 comments on commit 7d4b379

Please sign in to comment.