Spaces:
Sleeping
Sleeping
File size: 6,986 Bytes
cd6720a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | const INF = 1e20;
// lookup table for gamma-corrected, signed squared alpha distance values
const alphaTable = new Float64Array(256);
for (let i = 0; i < 256; i++) {
const d = 0.5 - Math.pow(i / 255, 1 / 2.2);
alphaTable[i] = d * Math.abs(d);
}
alphaTable[255] = -INF;
export default class TinySDF {
constructor({
fontSize = 24,
buffer = 3,
radius = 8,
cutoff = 0.25,
fontFamily = 'sans-serif',
fontWeight = 'normal',
fontStyle = 'normal',
lang = null
} = {}) {
this.buffer = buffer; // padding around a glyph's bounding box
this.radius = radius; // how many pixels around the glyph edge are encoded as signed distances
this.cutoff = cutoff; // how much of the SDF byte range represents inside vs outside the edge
this.lang = lang; // language of the Canvas drawing context
// make the canvas size big enough to both have the specified buffer around the glyph
// for "halo", and account for some glyphs possibly being larger than their font size
const size = this.size = fontSize + buffer * 4;
const canvas = this._createCanvas(size);
const ctx = this.ctx = canvas.getContext('2d', {willReadFrequently: true});
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left'; // Necessary so that RTL text doesn't have different alignment
ctx.fillStyle = 'black';
// two grids of squared distances: one for the outside of the glyph shape, one for the inside;
// the signed distance is derived as sqrt(outer) - sqrt(inner)
this.gridOuter = new Float64Array(size * size);
this.gridInner = new Float64Array(size * size);
this.f = new Float64Array(size);
this.z = new Float64Array(size + 1);
this.v = new Uint16Array(size);
}
_createCanvas(size) {
if (typeof OffscreenCanvas !== 'undefined') {
return new OffscreenCanvas(size, size);
}
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
return canvas;
}
draw(char) {
const {
width: glyphAdvance,
actualBoundingBoxAscent,
actualBoundingBoxDescent,
actualBoundingBoxLeft,
actualBoundingBoxRight
} = this.ctx.measureText(char);
// The integer/pixel part of the alignment is encoded in metrics.glyphTop/glyphLeft
// The remainder is implicitly encoded in the rasterization
const glyphTop = Math.ceil(actualBoundingBoxAscent);
// actualBoundingBoxLeft is positive when ink extends LEFT of the origin (per spec),
// so negate to get the ink's left edge in canvas x-coords (positive = right of origin)
const glyphLeft = Math.floor(-actualBoundingBoxLeft);
// If the glyph overflows the canvas size, it will be clipped at the bottom/right
const glyphWidth = Math.max(0, Math.min(this.size - this.buffer, Math.ceil(actualBoundingBoxRight) - glyphLeft));
const glyphHeight = Math.max(0, Math.min(this.size - this.buffer, glyphTop + Math.ceil(actualBoundingBoxDescent)));
const width = glyphWidth + 2 * this.buffer;
const height = glyphHeight + 2 * this.buffer;
const len = Math.max(width * height, 0);
const data = new Uint8ClampedArray(len);
const glyph = {data, width, height, glyphWidth, glyphHeight, glyphTop, glyphLeft, glyphAdvance};
if (glyphWidth === 0 || glyphHeight === 0) return glyph;
const {ctx, buffer, gridInner, gridOuter} = this;
if (this.lang) ctx.lang = this.lang;
ctx.clearRect(buffer, buffer, glyphWidth, glyphHeight);
ctx.fillText(char, buffer - glyphLeft, buffer + glyphTop);
const imgData = ctx.getImageData(buffer, buffer, glyphWidth, glyphHeight);
// default: outside the glyph (INF distance) for outer, inside (0 distance) for inner
gridOuter.fill(INF, 0, len);
gridInner.fill(0, 0, len);
// for anti-aliased pixels, treat partial coverage as a distance approximation:
// a fully covered pixel gets 0 outer / INF inner; a partial pixel gets a small
// non-zero outer or inner distance based on how far its coverage deviates from 0.5
let imgIdx = 3; // start at the alpha channel of the first pixel
for (let y = 0; y < glyphHeight; y++) {
let j = (y + buffer) * width + buffer;
for (let x = 0; x < glyphWidth; x++, imgIdx += 4, j++) {
const a = imgData.data[imgIdx]; // alpha value
if (a === 0) continue; // empty pixels
const t = alphaTable[a];
gridOuter[j] = Math.max(0, t);
gridInner[j] = Math.max(0, -t);
}
}
edt(gridOuter, 0, 0, width, height, width, this.f, this.v, this.z);
// Pad the inner EDT region by 1 px so ink pixels touching the bbox edge can see the
// outside-ink seeds in the buffer region; clamp to buffer so we don't underflow when buffer=0
const pad = Math.min(buffer, 1);
edt(gridInner, buffer - pad, buffer - pad, glyphWidth + 2 * pad, glyphHeight + 2 * pad, width, this.f, this.v, this.z);
// encode signed distance as a byte: inside the glyph maps to high values, outside to low,
// with the edge gradient spanning [-radius * cutoff, radius * (1 - cutoff)] pixels around the edge;
// Uint8ClampedArray clamps beyond that
const scale = 255 / this.radius;
const base = 255 * (1 - this.cutoff);
for (let i = 0; i < len; i++) {
const d = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]);
data[i] = Math.round(base - scale * d);
}
return glyph;
}
}
// 2D Euclidean squared distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/papers/dt-final.pdf
function edt(data, x0, y0, width, height, gridSize, f, v, z) {
for (let x = x0; x < x0 + width; x++) edt1d(data, y0 * gridSize + x, gridSize, height, f, v, z);
for (let y = y0; y < y0 + height; y++) edt1d(data, y * gridSize + x0, 1, width, f, v, z);
}
// 1D squared distance transform
function edt1d(grid, offset, stride, length, f, v, z) {
v[0] = 0;
z[0] = -INF;
z[1] = INF;
f[0] = grid[offset];
for (let q = 1, k = 0, s = 0; q < length; q++) {
f[q] = grid[offset + q * stride];
const q2 = q * q;
do {
const r = v[k];
s = (f[q] - f[r] + q2 - r * r) / (q - r) / 2;
} while (s <= z[k] && --k > -1);
k++;
v[k] = q;
z[k] = s;
z[k + 1] = INF;
}
for (let q = 0, k = 0; q < length; q++) {
while (z[k + 1] < q) k++;
const r = v[k];
const qr = q - r;
grid[offset + q * stride] = f[r] + qr * qr;
}
}
|