Favivon - Correção

This commit is contained in:
2026-05-30 19:59:39 -03:00
parent 76ddaa815d
commit d7dfd221f0
32859 changed files with 5459654 additions and 404 deletions
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2021 Jason Johnston
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+105
View File
@@ -0,0 +1,105 @@
# webgl-sdf-generator
This is a signed distance field (SDF) image generator for 2D paths such as font glyphs, for use in Web environments. It utilizes WebGL when possible for GPU-accelerated SDF generation.
## Usage
### Install it from npm:
```shell
npm install webgl-sdf-generator
```
[![NPM](https://nodei.co/npm/webgl-sdf-generator.png?compact=true)](https://npmjs.org/package/webgl-sdf-generator)
### Import and initialize:
```js
import initSDFGenerator from 'webgl-sdf-generator'
// or: const initSDFGenerator = require('webgl-sdf-generator')
const generator = initSDFGenerator()
```
The `webgl-sdf-generator` package's only export is a factory function which you _must invoke_ to return an object which holds various methods for performing the generation.
> _Why a factory function?_ The main reason is to ensure the entire module's code is wrapped within a single self-contained function with no closure dependencies. This enables that function to be stringified and passed into a web worker, for example.
Note that each factory call will result in its own internal WebGL context, which may be useful in some rare cases, but usually you'll just want to call it once and share that single generator object.
### Generate a single SDF:
```js
const sdfImageData = generator.generate(
64, // width
64, // height
'M0,0L50,25L25,50Z', // path
[-5, -5, 55, 55], // viewBox
25, // maxDistance
1 // exponent
)
```
Let's break down those arguments...
- **`width/height`** - The dimensions of the resulting image.
- **`path`** - An SVG-like [path string](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands). Only the following path commands are currently supported: `M`, `L`, `Q`, `C`, and `Z`.
- **`viewBox`** - The rectangle in the `path`'s coordinate system that will be covered by the output image. Specified as an array of `[left, top, right, bottom]`. You'll want to account for padding around the path shape in this rectangle.
- **`maxDistance`** - The maximum distance that will be encoded in the distance field; this is the distance from the path's edge at which the SDF value will be `0` or `255`.
- **`exponent`** - An optional exponent to apply to the SDF distance values as they get farther from the path's edge. This can be useful when `maxDistance` is large, to allow more precision near the path edge where it's more important and decreasing precision far away ([visualized here](https://www.desmos.com/calculator/uiaq5aqiam)). Whatever uses the SDF later on will need to invert the transformation to get useful distance values. Defaults to `1` for no curve.
The return value is a `Uint8Array` of SDF image pixel values (single channel), where 127.5 is the "zero distance" aligning with path edges. Values below that are outside the path and values above it are inside the path.
When you call `generator.generate(...)`, it will first attempt to build the SDF using WebGL; this is super fast because it is GPU-acclerated. This should work in most browsers, but if for whatever reason the proper WebGL support is not available or fails due to context loss then it will fall back to a slower JavaScript-based implementation.
If you want more control over this fallback behavior, you can access the individual implementations directly:
```js
// Same arguments as the main generate():
const resultFromGL = generator.webgl.generate(...args)
const resultFromJS = generator.javascript.generate(...args)
```
The WebGL implementation also provides method to detect support if you want to test it beforehand:
```js
const webglSupported = generator.webgl.isSupported()
```
### Generate an atlas of many SDFs:
Making a single SDF has its uses, but it's likely that you actually want that SDF to be added to a larger "atlas" image of many SDFs. While you could do that by calling `generator.generate()` as above and manually inserting the returned `Uint8Array` into a larger texture, that isn't optimal because it involves reading pixels back from the GPU for every SDF, which has a performance impact.
Instead, you can generate an SDF directly into a region+channel of a WebGL-enabled `canvas`, populating your "atlas" directly on the GPU. Only the region/channel for the new SDF will be overwritten, the rest will be preserved. That `canvas` can then be used as the source for a WebGL texture for rendering. This ends up being much faster since the data all stays on the GPU.
```js
generator.generateIntoCanvas(
64, // width
64, // height
'M0,0L50,25L25,50Z', // path
[-5, -5, 55, 55], // viewBox
25, // maxDistance
1, // exponent
yourCanvasElement, // output canvas
128, // output x coordinate
64, // output y coordinate
0 // output color channel
)
```
And similarly for the individual implementations:
```js
// Same arguments as the main generate():
generator.webgl.generateIntoCanvas(...args)
generator.javascript.generateIntoCanvas(...args)
```
Note that the canvas you pass:
- *must* be WebGL-enabled, meaning that `getContext('webgl')` will succeed for it; you can't give it a canvas with a `2d` context, for example.
- *should not* be shared with other processes that change the GL context state; `generateIntoCanvas` only sets the exact state it needs and makes no attempt to restore previous state, so sharing it could lead to unpredictable results.
+821
View File
@@ -0,0 +1,821 @@
export default function SDFGenerator() {
var exports = (function (exports) {
'use strict';
/**
* Find the point on a quadratic bezier curve at t where t is in the range [0, 1]
*/
function pointOnQuadraticBezier (x0, y0, x1, y1, x2, y2, t, pointOut) {
var t2 = 1 - t;
pointOut.x = t2 * t2 * x0 + 2 * t2 * t * x1 + t * t * x2;
pointOut.y = t2 * t2 * y0 + 2 * t2 * t * y1 + t * t * y2;
}
/**
* Find the point on a cubic bezier curve at t where t is in the range [0, 1]
*/
function pointOnCubicBezier (x0, y0, x1, y1, x2, y2, x3, y3, t, pointOut) {
var t2 = 1 - t;
pointOut.x = t2 * t2 * t2 * x0 + 3 * t2 * t2 * t * x1 + 3 * t2 * t * t * x2 + t * t * t * x3;
pointOut.y = t2 * t2 * t2 * y0 + 3 * t2 * t2 * t * y1 + 3 * t2 * t * t * y2 + t * t * t * y3;
}
/**
* Parse a path string into its constituent line/curve commands, invoking a callback for each.
* @param {string} pathString - An SVG-like path string to parse; should only contain commands: M/L/Q/C/Z
* @param {function(
* command: 'L'|'Q'|'C',
* startX: number,
* startY: number,
* endX: number,
* endY: number,
* ctrl1X?: number,
* ctrl1Y?: number,
* ctrl2X?: number,
* ctrl2Y?: number
* )} commandCallback - A callback function that will be called once for each parsed path command, passing the
* command identifier (only L/Q/C commands) and its numeric arguments.
*/
function forEachPathCommand(pathString, commandCallback) {
var segmentRE = /([MLQCZ])([^MLQCZ]*)/g;
var match, firstX, firstY, prevX, prevY;
while ((match = segmentRE.exec(pathString))) {
var args = match[2]
.replace(/^\s*|\s*$/g, '')
.split(/[,\s]+/)
.map(function (v) { return parseFloat(v); });
switch (match[1]) {
case 'M':
prevX = firstX = args[0];
prevY = firstY = args[1];
break
case 'L':
if (args[0] !== prevX || args[1] !== prevY) { // yup, some fonts have zero-length line commands
commandCallback('L', prevX, prevY, (prevX = args[0]), (prevY = args[1]));
}
break
case 'Q': {
commandCallback('Q', prevX, prevY, (prevX = args[2]), (prevY = args[3]), args[0], args[1]);
break
}
case 'C': {
commandCallback('C', prevX, prevY, (prevX = args[4]), (prevY = args[5]), args[0], args[1], args[2], args[3]);
break
}
case 'Z':
if (prevX !== firstX || prevY !== firstY) {
commandCallback('L', prevX, prevY, firstX, firstY);
}
break
}
}
}
/**
* Convert a path string to a series of straight line segments
* @param {string} pathString - An SVG-like path string to parse; should only contain commands: M/L/Q/C/Z
* @param {function(x1:number, y1:number, x2:number, y2:number)} segmentCallback - A callback
* function that will be called once for every line segment
* @param {number} [curvePoints] - How many straight line segments to use when approximating a
* bezier curve in the path. Defaults to 16.
*/
function pathToLineSegments (pathString, segmentCallback, curvePoints) {
if ( curvePoints === void 0 ) curvePoints = 16;
var tempPoint = { x: 0, y: 0 };
forEachPathCommand(pathString, function (command, startX, startY, endX, endY, ctrl1X, ctrl1Y, ctrl2X, ctrl2Y) {
switch (command) {
case 'L':
segmentCallback(startX, startY, endX, endY);
break
case 'Q': {
var prevCurveX = startX;
var prevCurveY = startY;
for (var i = 1; i < curvePoints; i++) {
pointOnQuadraticBezier(
startX, startY,
ctrl1X, ctrl1Y,
endX, endY,
i / (curvePoints - 1),
tempPoint
);
segmentCallback(prevCurveX, prevCurveY, tempPoint.x, tempPoint.y);
prevCurveX = tempPoint.x;
prevCurveY = tempPoint.y;
}
break
}
case 'C': {
var prevCurveX$1 = startX;
var prevCurveY$1 = startY;
for (var i$1 = 1; i$1 < curvePoints; i$1++) {
pointOnCubicBezier(
startX, startY,
ctrl1X, ctrl1Y,
ctrl2X, ctrl2Y,
endX, endY,
i$1 / (curvePoints - 1),
tempPoint
);
segmentCallback(prevCurveX$1, prevCurveY$1, tempPoint.x, tempPoint.y);
prevCurveX$1 = tempPoint.x;
prevCurveY$1 = tempPoint.y;
}
break
}
}
});
}
var viewportQuadVertex = "precision highp float;attribute vec2 aUV;varying vec2 vUV;void main(){vUV=aUV;gl_Position=vec4(mix(vec2(-1.0),vec2(1.0),aUV),0.0,1.0);}";
var copyTexFragment = "precision highp float;uniform sampler2D tex;varying vec2 vUV;void main(){gl_FragColor=texture2D(tex,vUV);}";
var cache = new WeakMap();
var glContextParams = {
premultipliedAlpha: false,
preserveDrawingBuffer: true,
antialias: false,
depth: false,
};
/**
* This is a little helper library for WebGL. It assists with state management for a GL context.
* It's pretty tightly wrapped to the needs of this package, not very general-purpose.
*
* @param { WebGLRenderingContext | HTMLCanvasElement | OffscreenCanvas } glOrCanvas - the GL context to wrap
* @param { ({gl, getExtension, withProgram, withTexture, withTextureFramebuffer, handleContextLoss}) => void } callback
*/
function withWebGLContext (glOrCanvas, callback) {
var gl = glOrCanvas.getContext ? glOrCanvas.getContext('webgl', glContextParams) : glOrCanvas;
var wrapper = cache.get(gl);
if (!wrapper) {
var isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
var extensions = {};
var programs = {};
var textures = {};
var textureUnit = -1;
var framebufferStack = [];
gl.canvas.addEventListener('webglcontextlost', function (e) {
handleContextLoss();
e.preventDefault();
}, false);
function getExtension (name) {
var ext = extensions[name];
if (!ext) {
ext = extensions[name] = gl.getExtension(name);
if (!ext) {
throw new Error((name + " not supported"))
}
}
return ext
}
function compileShader (src, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
// const status = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
// if (!status && !gl.isContextLost()) {
// throw new Error(gl.getShaderInfoLog(shader).trim())
// }
return shader
}
function withProgram (name, vert, frag, func) {
if (!programs[name]) {
var attributes = {};
var uniforms = {};
var program = gl.createProgram();
gl.attachShader(program, compileShader(vert, gl.VERTEX_SHADER));
gl.attachShader(program, compileShader(frag, gl.FRAGMENT_SHADER));
gl.linkProgram(program);
programs[name] = {
program: program,
transaction: function transaction (func) {
gl.useProgram(program);
func({
setUniform: function setUniform (type, name) {
var values = [], len = arguments.length - 2;
while ( len-- > 0 ) values[ len ] = arguments[ len + 2 ];
var uniformLoc = uniforms[name] || (uniforms[name] = gl.getUniformLocation(program, name));
gl[("uniform" + type)].apply(gl, [ uniformLoc ].concat( values ));
},
setAttribute: function setAttribute (name, size, usage, instancingDivisor, data) {
var attr = attributes[name];
if (!attr) {
attr = attributes[name] = {
buf: gl.createBuffer(), // TODO should we destroy our buffers?
loc: gl.getAttribLocation(program, name),
data: null
};
}
gl.bindBuffer(gl.ARRAY_BUFFER, attr.buf);
gl.vertexAttribPointer(attr.loc, size, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(attr.loc);
if (isWebGL2) {
gl.vertexAttribDivisor(attr.loc, instancingDivisor);
} else {
getExtension('ANGLE_instanced_arrays').vertexAttribDivisorANGLE(attr.loc, instancingDivisor);
}
if (data !== attr.data) {
gl.bufferData(gl.ARRAY_BUFFER, data, usage);
attr.data = data;
}
}
});
}
};
}
programs[name].transaction(func);
}
function withTexture (name, func) {
textureUnit++;
try {
gl.activeTexture(gl.TEXTURE0 + textureUnit);
var texture = textures[name];
if (!texture) {
texture = textures[name] = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
}
gl.bindTexture(gl.TEXTURE_2D, texture);
func(texture, textureUnit);
} finally {
textureUnit--;
}
}
function withTextureFramebuffer (texture, textureUnit, func) {
var framebuffer = gl.createFramebuffer();
framebufferStack.push(framebuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
try {
func(framebuffer);
} finally {
gl.deleteFramebuffer(framebuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebufferStack[--framebufferStack.length - 1] || null);
}
}
function handleContextLoss () {
extensions = {};
programs = {};
textures = {};
textureUnit = -1;
framebufferStack.length = 0;
}
cache.set(gl, wrapper = {
gl: gl,
isWebGL2: isWebGL2,
getExtension: getExtension,
withProgram: withProgram,
withTexture: withTexture,
withTextureFramebuffer: withTextureFramebuffer,
handleContextLoss: handleContextLoss,
});
}
callback(wrapper);
}
function renderImageData(glOrCanvas, imageData, x, y, width, height, channels, framebuffer) {
if ( channels === void 0 ) channels = 15;
if ( framebuffer === void 0 ) framebuffer = null;
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var withProgram = ref.withProgram;
var withTexture = ref.withTexture;
withTexture('copy', function (tex, texUnit) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imageData);
withProgram('copy', viewportQuadVertex, copyTexFragment, function (ref) {
var setUniform = ref.setUniform;
var setAttribute = ref.setAttribute;
setAttribute('aUV', 2, gl.STATIC_DRAW, 0, new Float32Array([0, 0, 2, 0, 0, 2]));
setUniform('1i', 'image', texUnit);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer || null);
gl.disable(gl.BLEND);
gl.colorMask(channels & 8, channels & 4, channels & 2, channels & 1);
gl.viewport(x, y, width, height);
gl.scissor(x, y, width, height);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
});
});
}
/**
* Resizing a canvas clears its contents; this utility copies the previous contents over.
* @param canvas
* @param newWidth
* @param newHeight
*/
function resizeWebGLCanvasWithoutClearing(canvas, newWidth, newHeight) {
var width = canvas.width;
var height = canvas.height;
withWebGLContext(canvas, function (ref) {
var gl = ref.gl;
var data = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
canvas.width = newWidth;
canvas.height = newHeight;
renderImageData(gl, data, 0, 0, width, height);
});
}
var webglUtils = /*#__PURE__*/Object.freeze({
__proto__: null,
withWebGLContext: withWebGLContext,
renderImageData: renderImageData,
resizeWebGLCanvasWithoutClearing: resizeWebGLCanvasWithoutClearing
});
function generate$2 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
var textureData = new Uint8Array(sdfWidth * sdfHeight);
var viewBoxWidth = viewBox[2] - viewBox[0];
var viewBoxHeight = viewBox[3] - viewBox[1];
// Decompose all paths into straight line segments and add them to an index
var segments = [];
pathToLineSegments(path, function (x1, y1, x2, y2) {
segments.push({
x1: x1, y1: y1, x2: x2, y2: y2,
minX: Math.min(x1, x2),
minY: Math.min(y1, y2),
maxX: Math.max(x1, x2),
maxY: Math.max(y1, y2)
});
});
// Sort segments by maxX, this will let us short-circuit some loops below
segments.sort(function (a, b) { return a.maxX - b.maxX; });
// For each target SDF texel, find the distance from its center to its nearest line segment,
// map that distance to an alpha value, and write that alpha to the texel
for (var sdfX = 0; sdfX < sdfWidth; sdfX++) {
for (var sdfY = 0; sdfY < sdfHeight; sdfY++) {
var signedDist = findNearestSignedDistance(
viewBox[0] + viewBoxWidth * (sdfX + 0.5) / sdfWidth,
viewBox[1] + viewBoxHeight * (sdfY + 0.5) / sdfHeight
);
// Use an exponential scale to ensure the texels very near the glyph path have adequate
// precision, while allowing the distance field to cover the entire texture, given that
// there are only 8 bits available. Formula visualized: https://www.desmos.com/calculator/uiaq5aqiam
var alpha = Math.pow((1 - Math.abs(signedDist) / maxDistance), sdfExponent) / 2;
if (signedDist < 0) {
alpha = 1 - alpha;
}
alpha = Math.max(0, Math.min(255, Math.round(alpha * 255))); //clamp
textureData[sdfY * sdfWidth + sdfX] = alpha;
}
}
return textureData
/**
* For a given x/y, search the index for the closest line segment and return
* its signed distance. Negative = inside, positive = outside, zero = on edge
* @param x
* @param y
* @returns {number}
*/
function findNearestSignedDistance (x, y) {
var closestDistSq = Infinity;
var closestDist = Infinity;
for (var i = segments.length; i--;) {
var seg = segments[i];
if (seg.maxX + closestDist <= x) { break } //sorting by maxX means no more can be closer, so we can short-circuit
if (x + closestDist > seg.minX && y - closestDist < seg.maxY && y + closestDist > seg.minY) {
var distSq = absSquareDistanceToLineSegment(x, y, seg.x1, seg.y1, seg.x2, seg.y2);
if (distSq < closestDistSq) {
closestDistSq = distSq;
closestDist = Math.sqrt(closestDistSq);
}
}
}
// Flip to negative distance if inside the poly
if (isPointInPoly(x, y)) {
closestDist = -closestDist;
}
return closestDist
}
/**
* Determine whether the given point lies inside or outside the glyph. Uses a simple
* winding-number ray casting algorithm using a ray pointing east from the point.
*/
function isPointInPoly (x, y) {
var winding = 0;
for (var i = segments.length; i--;) {
var seg = segments[i];
if (seg.maxX <= x) { break } //sorting by maxX means no more can cross, so we can short-circuit
var intersects = ((seg.y1 > y) !== (seg.y2 > y)) && (x < (seg.x2 - seg.x1) * (y - seg.y1) / (seg.y2 - seg.y1) + seg.x1);
if (intersects) {
winding += seg.y1 < seg.y2 ? 1 : -1;
}
}
return winding !== 0
}
}
function generateIntoCanvas$2(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
generateIntoFramebuffer$1(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, null, x, y, channel);
}
function generateIntoFramebuffer$1 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas, framebuffer, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
var data = generate$2(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent);
// Expand single-channel data to rbga
var rgbaData = new Uint8Array(data.length * 4);
for (var i = 0; i < data.length; i++) {
rgbaData[i * 4 + channel] = data[i];
}
renderImageData(glOrCanvas, rgbaData, x, y, sdfWidth, sdfHeight, 1 << (3 - channel), framebuffer);
}
/**
* Find the absolute distance from a point to a line segment at closest approach
*/
function absSquareDistanceToLineSegment (x, y, lineX0, lineY0, lineX1, lineY1) {
var ldx = lineX1 - lineX0;
var ldy = lineY1 - lineY0;
var lengthSq = ldx * ldx + ldy * ldy;
var t = lengthSq ? Math.max(0, Math.min(1, ((x - lineX0) * ldx + (y - lineY0) * ldy) / lengthSq)) : 0;
var dx = x - (lineX0 + t * ldx);
var dy = y - (lineY0 + t * ldy);
return dx * dx + dy * dy
}
var javascript = /*#__PURE__*/Object.freeze({
__proto__: null,
generate: generate$2,
generateIntoCanvas: generateIntoCanvas$2,
generateIntoFramebuffer: generateIntoFramebuffer$1
});
var mainVertex = "precision highp float;uniform vec4 uGlyphBounds;attribute vec2 aUV;attribute vec4 aLineSegment;varying vec4 vLineSegment;varying vec2 vGlyphXY;void main(){vLineSegment=aLineSegment;vGlyphXY=mix(uGlyphBounds.xy,uGlyphBounds.zw,aUV);gl_Position=vec4(mix(vec2(-1.0),vec2(1.0),aUV),0.0,1.0);}";
var mainFragment = "precision highp float;uniform vec4 uGlyphBounds;uniform float uMaxDistance;uniform float uExponent;varying vec4 vLineSegment;varying vec2 vGlyphXY;float absDistToSegment(vec2 point,vec2 lineA,vec2 lineB){vec2 lineDir=lineB-lineA;float lenSq=dot(lineDir,lineDir);float t=lenSq==0.0 ? 0.0 : clamp(dot(point-lineA,lineDir)/lenSq,0.0,1.0);vec2 linePt=lineA+t*lineDir;return distance(point,linePt);}void main(){vec4 seg=vLineSegment;vec2 p=vGlyphXY;float dist=absDistToSegment(p,seg.xy,seg.zw);float val=pow(1.0-clamp(dist/uMaxDistance,0.0,1.0),uExponent)*0.5;bool crossing=(seg.y>p.y!=seg.w>p.y)&&(p.x<(seg.z-seg.x)*(p.y-seg.y)/(seg.w-seg.y)+seg.x);bool crossingUp=crossing&&vLineSegment.y<vLineSegment.w;gl_FragColor=vec4(crossingUp ? 1.0/255.0 : 0.0,crossing&&!crossingUp ? 1.0/255.0 : 0.0,0.0,val);}";
var postFragment = "precision highp float;uniform sampler2D tex;varying vec2 vUV;void main(){vec4 color=texture2D(tex,vUV);bool inside=color.r!=color.g;float val=inside ? 1.0-color.a : color.a;gl_FragColor=vec4(val);}";
// Single triangle covering viewport
var viewportUVs = new Float32Array([0, 0, 2, 0, 0, 2]);
var implicitContext = null;
var isTestingSupport = false;
var NULL_OBJECT = {};
var supportByCanvas = new WeakMap(); // canvas -> bool
function validateSupport (glOrCanvas) {
if (!isTestingSupport && !isSupported(glOrCanvas)) {
throw new Error('WebGL generation not supported')
}
}
function generate$1 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( glOrCanvas === void 0 ) glOrCanvas = null;
if (!glOrCanvas) {
glOrCanvas = implicitContext;
if (!glOrCanvas) {
var canvas = typeof OffscreenCanvas === 'function'
? new OffscreenCanvas(1, 1)
: typeof document !== 'undefined'
? document.createElement('canvas')
: null;
if (!canvas) {
throw new Error('OffscreenCanvas or DOM canvas not supported')
}
glOrCanvas = implicitContext = canvas.getContext('webgl', { depth: false });
}
}
validateSupport(glOrCanvas);
var rgbaData = new Uint8Array(sdfWidth * sdfHeight * 4); //not Uint8ClampedArray, cuz Safari
// Render into a background texture framebuffer
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var withTexture = ref.withTexture;
var withTextureFramebuffer = ref.withTextureFramebuffer;
withTexture('readable', function (texture, textureUnit) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, sdfWidth, sdfHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
withTextureFramebuffer(texture, textureUnit, function (framebuffer) {
generateIntoFramebuffer(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent,
gl,
framebuffer,
0,
0,
0 // red channel
);
gl.readPixels(0, 0, sdfWidth, sdfHeight, gl.RGBA, gl.UNSIGNED_BYTE, rgbaData);
});
});
});
// Throw away all but the red channel
var data = new Uint8Array(sdfWidth * sdfHeight);
for (var i = 0, j = 0; i < rgbaData.length; i += 4) {
data[j++] = rgbaData[i];
}
return data
}
function generateIntoCanvas$1(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
generateIntoFramebuffer(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, null, x, y, channel);
}
function generateIntoFramebuffer (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas, framebuffer, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
// Verify support
validateSupport(glOrCanvas);
// Compute path segments
var lineSegmentCoords = [];
pathToLineSegments(path, function (x1, y1, x2, y2) {
lineSegmentCoords.push(x1, y1, x2, y2);
});
lineSegmentCoords = new Float32Array(lineSegmentCoords);
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var isWebGL2 = ref.isWebGL2;
var getExtension = ref.getExtension;
var withProgram = ref.withProgram;
var withTexture = ref.withTexture;
var withTextureFramebuffer = ref.withTextureFramebuffer;
var handleContextLoss = ref.handleContextLoss;
withTexture('rawDistances', function (intermediateTexture, intermediateTextureUnit) {
if (sdfWidth !== intermediateTexture._lastWidth || sdfHeight !== intermediateTexture._lastHeight) {
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA,
intermediateTexture._lastWidth = sdfWidth,
intermediateTexture._lastHeight = sdfHeight,
0, gl.RGBA, gl.UNSIGNED_BYTE, null
);
}
// Unsigned distance pass
withProgram('main', mainVertex, mainFragment, function (ref) {
var setAttribute = ref.setAttribute;
var setUniform = ref.setUniform;
// Init extensions
var instancingExtension = !isWebGL2 && getExtension('ANGLE_instanced_arrays');
var blendMinMaxExtension = !isWebGL2 && getExtension('EXT_blend_minmax');
// Init/update attributes
setAttribute('aUV', 2, gl.STATIC_DRAW, 0, viewportUVs);
setAttribute('aLineSegment', 4, gl.DYNAMIC_DRAW, 1, lineSegmentCoords);
// Init/update uniforms
setUniform.apply(void 0, [ '4f', 'uGlyphBounds' ].concat( viewBox ));
setUniform('1f', 'uMaxDistance', maxDistance);
setUniform('1f', 'uExponent', sdfExponent);
// Render initial unsigned distance / winding number info to a texture
withTextureFramebuffer(intermediateTexture, intermediateTextureUnit, function (framebuffer) {
gl.enable(gl.BLEND);
gl.colorMask(true, true, true, true);
gl.viewport(0, 0, sdfWidth, sdfHeight);
gl.scissor(0, 0, sdfWidth, sdfHeight);
gl.blendFunc(gl.ONE, gl.ONE);
// Red+Green channels are incremented (FUNC_ADD) for segment-ray crossings to give a "winding number".
// Alpha holds the closest (MAX) unsigned distance.
gl.blendEquationSeparate(gl.FUNC_ADD, isWebGL2 ? gl.MAX : blendMinMaxExtension.MAX_EXT);
gl.clear(gl.COLOR_BUFFER_BIT);
if (isWebGL2) {
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, lineSegmentCoords.length / 4);
} else {
instancingExtension.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 3, lineSegmentCoords.length / 4);
}
// Debug
// const debug = new Uint8Array(sdfWidth * sdfHeight * 4)
// gl.readPixels(0, 0, sdfWidth, sdfHeight, gl.RGBA, gl.UNSIGNED_BYTE, debug)
// console.log('intermediate texture data: ', debug)
});
});
// Use the data stored in the texture to apply inside/outside and write to the output framebuffer rect+channel.
withProgram('post', viewportQuadVertex, postFragment, function (program) {
program.setAttribute('aUV', 2, gl.STATIC_DRAW, 0, viewportUVs);
program.setUniform('1i', 'tex', intermediateTextureUnit);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.disable(gl.BLEND);
gl.colorMask(channel === 0, channel === 1, channel === 2, channel === 3);
gl.viewport(x, y, sdfWidth, sdfHeight);
gl.scissor(x, y, sdfWidth, sdfHeight);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
});
// Handle context loss occurring during any of the above calls
if (gl.isContextLost()) {
handleContextLoss();
throw new Error('webgl context lost')
}
});
}
function isSupported (glOrCanvas) {
var key = (!glOrCanvas || glOrCanvas === implicitContext) ? NULL_OBJECT : (glOrCanvas.canvas || glOrCanvas);
var supported = supportByCanvas.get(key);
if (supported === undefined) {
isTestingSupport = true;
var failReason = null;
try {
// Since we can't detect all failure modes up front, let's just do a trial run of a
// simple path and compare what we get back to the correct expected result. This will
// also serve to prime the shader compilation.
var expectedResult = [
97, 106, 97, 61,
99, 137, 118, 80,
80, 118, 137, 99,
61, 97, 106, 97
];
var testResult = generate$1(
4,
4,
'M8,8L16,8L24,24L16,24Z',
[0, 0, 32, 32],
24,
1,
glOrCanvas
);
supported = testResult && expectedResult.length === testResult.length &&
testResult.every(function (val, i) { return val === expectedResult[i]; });
if (!supported) {
failReason = 'bad trial run results';
console.info(expectedResult, testResult);
}
} catch (err) {
// TODO if it threw due to webgl context loss, should we maybe leave isSupported as null and try again later?
supported = false;
failReason = err.message;
}
if (failReason) {
console.warn('WebGL SDF generation not supported:', failReason);
}
isTestingSupport = false;
supportByCanvas.set(key, supported);
}
return supported
}
var webgl = /*#__PURE__*/Object.freeze({
__proto__: null,
generate: generate$1,
generateIntoCanvas: generateIntoCanvas$1,
generateIntoFramebuffer: generateIntoFramebuffer,
isSupported: isSupported
});
/**
* Generate an SDF texture image for a 2D path.
*
* @param {number} sdfWidth - width of the SDF output image in pixels.
* @param {number} sdfHeight - height of the SDF output image in pixels.
* @param {string} path - an SVG-like path string describing the glyph; should only contain commands: M/L/Q/C/Z.
* @param {number[]} viewBox - [minX, minY, maxX, maxY] in font units aligning with the texture's edges.
* @param {number} maxDistance - the maximum distance from the glyph path in font units that will be encoded; defaults
* to half the maximum viewBox dimension.
* @param {number} [sdfExponent] - specifies an exponent for encoding the SDF's distance values; higher exponents
* will give greater precision nearer the glyph's path.
* @return {Uint8Array}
*/
function generate(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent
) {
if ( maxDistance === void 0 ) maxDistance = Math.max(viewBox[2] - viewBox[0], viewBox[3] - viewBox[1]) / 2;
if ( sdfExponent === void 0 ) sdfExponent = 1;
try {
return generate$1.apply(webgl, arguments)
} catch(e) {
console.info('WebGL SDF generation failed, falling back to JS', e);
return generate$2.apply(javascript, arguments)
}
}
/**
* Generate an SDF texture image for a 2D path, inserting the result into a WebGL `canvas` at a given x/y position
* and color channel. This is generally much faster than calling `generate` because it does not require reading pixels
* back from the GPU->CPU -- the `canvas` can be used directly as a WebGL texture image, so it all stays on the GPU.
*
* @param {number} sdfWidth - width of the SDF output image in pixels.
* @param {number} sdfHeight - height of the SDF output image in pixels.
* @param {string} path - an SVG-like path string describing the glyph; should only contain commands: M/L/Q/C/Z.
* @param {number[]} viewBox - [minX, minY, maxX, maxY] in font units aligning with the texture's edges.
* @param {number} maxDistance - the maximum distance from the glyph path in font units that will be encoded; defaults
* to half the maximum viewBox dimension.
* @param {number} [sdfExponent] - specifies an exponent for encoding the SDF's distance values; higher exponents
* will give greater precision nearer the glyph's path.
* @param {HTMLCanvasElement|OffscreenCanvas} canvas - a WebGL-enabled canvas into which the SDF will be rendered.
* Only the relevant rect/channel will be modified, the rest will be preserved. To avoid unpredictable results
* due to shared GL context state, this canvas should be dedicated to use by this library alone.
* @param {number} x - the x position at which to render the SDF.
* @param {number} y - the y position at which to render the SDF.
* @param {number} channel - the color channel index (0-4) into which the SDF will be rendered.
* @return {Uint8Array}
*/
function generateIntoCanvas(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent,
canvas,
x,
y,
channel
) {
if ( maxDistance === void 0 ) maxDistance = Math.max(viewBox[2] - viewBox[0], viewBox[3] - viewBox[1]) / 2;
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
try {
return generateIntoCanvas$1.apply(webgl, arguments)
} catch(e) {
console.info('WebGL SDF generation failed, falling back to JS', e);
return generateIntoCanvas$2.apply(javascript, arguments)
}
}
exports.forEachPathCommand = forEachPathCommand;
exports.generate = generate;
exports.generateIntoCanvas = generateIntoCanvas;
exports.javascript = javascript;
exports.pathToLineSegments = pathToLineSegments;
exports.webgl = webgl;
exports.webglUtils = webglUtils;
Object.defineProperty(exports, '__esModule', { value: true });
return exports;
}({}));
return exports
}
+830
View File
@@ -0,0 +1,830 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.webgl_sdf_generator = factory());
}(this, (function () { 'use strict';
function SDFGenerator() {
var exports = (function (exports) {
/**
* Find the point on a quadratic bezier curve at t where t is in the range [0, 1]
*/
function pointOnQuadraticBezier (x0, y0, x1, y1, x2, y2, t, pointOut) {
var t2 = 1 - t;
pointOut.x = t2 * t2 * x0 + 2 * t2 * t * x1 + t * t * x2;
pointOut.y = t2 * t2 * y0 + 2 * t2 * t * y1 + t * t * y2;
}
/**
* Find the point on a cubic bezier curve at t where t is in the range [0, 1]
*/
function pointOnCubicBezier (x0, y0, x1, y1, x2, y2, x3, y3, t, pointOut) {
var t2 = 1 - t;
pointOut.x = t2 * t2 * t2 * x0 + 3 * t2 * t2 * t * x1 + 3 * t2 * t * t * x2 + t * t * t * x3;
pointOut.y = t2 * t2 * t2 * y0 + 3 * t2 * t2 * t * y1 + 3 * t2 * t * t * y2 + t * t * t * y3;
}
/**
* Parse a path string into its constituent line/curve commands, invoking a callback for each.
* @param {string} pathString - An SVG-like path string to parse; should only contain commands: M/L/Q/C/Z
* @param {function(
* command: 'L'|'Q'|'C',
* startX: number,
* startY: number,
* endX: number,
* endY: number,
* ctrl1X?: number,
* ctrl1Y?: number,
* ctrl2X?: number,
* ctrl2Y?: number
* )} commandCallback - A callback function that will be called once for each parsed path command, passing the
* command identifier (only L/Q/C commands) and its numeric arguments.
*/
function forEachPathCommand(pathString, commandCallback) {
var segmentRE = /([MLQCZ])([^MLQCZ]*)/g;
var match, firstX, firstY, prevX, prevY;
while ((match = segmentRE.exec(pathString))) {
var args = match[2]
.replace(/^\s*|\s*$/g, '')
.split(/[,\s]+/)
.map(function (v) { return parseFloat(v); });
switch (match[1]) {
case 'M':
prevX = firstX = args[0];
prevY = firstY = args[1];
break
case 'L':
if (args[0] !== prevX || args[1] !== prevY) { // yup, some fonts have zero-length line commands
commandCallback('L', prevX, prevY, (prevX = args[0]), (prevY = args[1]));
}
break
case 'Q': {
commandCallback('Q', prevX, prevY, (prevX = args[2]), (prevY = args[3]), args[0], args[1]);
break
}
case 'C': {
commandCallback('C', prevX, prevY, (prevX = args[4]), (prevY = args[5]), args[0], args[1], args[2], args[3]);
break
}
case 'Z':
if (prevX !== firstX || prevY !== firstY) {
commandCallback('L', prevX, prevY, firstX, firstY);
}
break
}
}
}
/**
* Convert a path string to a series of straight line segments
* @param {string} pathString - An SVG-like path string to parse; should only contain commands: M/L/Q/C/Z
* @param {function(x1:number, y1:number, x2:number, y2:number)} segmentCallback - A callback
* function that will be called once for every line segment
* @param {number} [curvePoints] - How many straight line segments to use when approximating a
* bezier curve in the path. Defaults to 16.
*/
function pathToLineSegments (pathString, segmentCallback, curvePoints) {
if ( curvePoints === void 0 ) curvePoints = 16;
var tempPoint = { x: 0, y: 0 };
forEachPathCommand(pathString, function (command, startX, startY, endX, endY, ctrl1X, ctrl1Y, ctrl2X, ctrl2Y) {
switch (command) {
case 'L':
segmentCallback(startX, startY, endX, endY);
break
case 'Q': {
var prevCurveX = startX;
var prevCurveY = startY;
for (var i = 1; i < curvePoints; i++) {
pointOnQuadraticBezier(
startX, startY,
ctrl1X, ctrl1Y,
endX, endY,
i / (curvePoints - 1),
tempPoint
);
segmentCallback(prevCurveX, prevCurveY, tempPoint.x, tempPoint.y);
prevCurveX = tempPoint.x;
prevCurveY = tempPoint.y;
}
break
}
case 'C': {
var prevCurveX$1 = startX;
var prevCurveY$1 = startY;
for (var i$1 = 1; i$1 < curvePoints; i$1++) {
pointOnCubicBezier(
startX, startY,
ctrl1X, ctrl1Y,
ctrl2X, ctrl2Y,
endX, endY,
i$1 / (curvePoints - 1),
tempPoint
);
segmentCallback(prevCurveX$1, prevCurveY$1, tempPoint.x, tempPoint.y);
prevCurveX$1 = tempPoint.x;
prevCurveY$1 = tempPoint.y;
}
break
}
}
});
}
var viewportQuadVertex = "precision highp float;attribute vec2 aUV;varying vec2 vUV;void main(){vUV=aUV;gl_Position=vec4(mix(vec2(-1.0),vec2(1.0),aUV),0.0,1.0);}";
var copyTexFragment = "precision highp float;uniform sampler2D tex;varying vec2 vUV;void main(){gl_FragColor=texture2D(tex,vUV);}";
var cache = new WeakMap();
var glContextParams = {
premultipliedAlpha: false,
preserveDrawingBuffer: true,
antialias: false,
depth: false,
};
/**
* This is a little helper library for WebGL. It assists with state management for a GL context.
* It's pretty tightly wrapped to the needs of this package, not very general-purpose.
*
* @param { WebGLRenderingContext | HTMLCanvasElement | OffscreenCanvas } glOrCanvas - the GL context to wrap
* @param { ({gl, getExtension, withProgram, withTexture, withTextureFramebuffer, handleContextLoss}) => void } callback
*/
function withWebGLContext (glOrCanvas, callback) {
var gl = glOrCanvas.getContext ? glOrCanvas.getContext('webgl', glContextParams) : glOrCanvas;
var wrapper = cache.get(gl);
if (!wrapper) {
var isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
var extensions = {};
var programs = {};
var textures = {};
var textureUnit = -1;
var framebufferStack = [];
gl.canvas.addEventListener('webglcontextlost', function (e) {
handleContextLoss();
e.preventDefault();
}, false);
function getExtension (name) {
var ext = extensions[name];
if (!ext) {
ext = extensions[name] = gl.getExtension(name);
if (!ext) {
throw new Error((name + " not supported"))
}
}
return ext
}
function compileShader (src, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
// const status = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
// if (!status && !gl.isContextLost()) {
// throw new Error(gl.getShaderInfoLog(shader).trim())
// }
return shader
}
function withProgram (name, vert, frag, func) {
if (!programs[name]) {
var attributes = {};
var uniforms = {};
var program = gl.createProgram();
gl.attachShader(program, compileShader(vert, gl.VERTEX_SHADER));
gl.attachShader(program, compileShader(frag, gl.FRAGMENT_SHADER));
gl.linkProgram(program);
programs[name] = {
program: program,
transaction: function transaction (func) {
gl.useProgram(program);
func({
setUniform: function setUniform (type, name) {
var values = [], len = arguments.length - 2;
while ( len-- > 0 ) values[ len ] = arguments[ len + 2 ];
var uniformLoc = uniforms[name] || (uniforms[name] = gl.getUniformLocation(program, name));
gl[("uniform" + type)].apply(gl, [ uniformLoc ].concat( values ));
},
setAttribute: function setAttribute (name, size, usage, instancingDivisor, data) {
var attr = attributes[name];
if (!attr) {
attr = attributes[name] = {
buf: gl.createBuffer(), // TODO should we destroy our buffers?
loc: gl.getAttribLocation(program, name),
data: null
};
}
gl.bindBuffer(gl.ARRAY_BUFFER, attr.buf);
gl.vertexAttribPointer(attr.loc, size, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(attr.loc);
if (isWebGL2) {
gl.vertexAttribDivisor(attr.loc, instancingDivisor);
} else {
getExtension('ANGLE_instanced_arrays').vertexAttribDivisorANGLE(attr.loc, instancingDivisor);
}
if (data !== attr.data) {
gl.bufferData(gl.ARRAY_BUFFER, data, usage);
attr.data = data;
}
}
});
}
};
}
programs[name].transaction(func);
}
function withTexture (name, func) {
textureUnit++;
try {
gl.activeTexture(gl.TEXTURE0 + textureUnit);
var texture = textures[name];
if (!texture) {
texture = textures[name] = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
}
gl.bindTexture(gl.TEXTURE_2D, texture);
func(texture, textureUnit);
} finally {
textureUnit--;
}
}
function withTextureFramebuffer (texture, textureUnit, func) {
var framebuffer = gl.createFramebuffer();
framebufferStack.push(framebuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
try {
func(framebuffer);
} finally {
gl.deleteFramebuffer(framebuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebufferStack[--framebufferStack.length - 1] || null);
}
}
function handleContextLoss () {
extensions = {};
programs = {};
textures = {};
textureUnit = -1;
framebufferStack.length = 0;
}
cache.set(gl, wrapper = {
gl: gl,
isWebGL2: isWebGL2,
getExtension: getExtension,
withProgram: withProgram,
withTexture: withTexture,
withTextureFramebuffer: withTextureFramebuffer,
handleContextLoss: handleContextLoss,
});
}
callback(wrapper);
}
function renderImageData(glOrCanvas, imageData, x, y, width, height, channels, framebuffer) {
if ( channels === void 0 ) channels = 15;
if ( framebuffer === void 0 ) framebuffer = null;
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var withProgram = ref.withProgram;
var withTexture = ref.withTexture;
withTexture('copy', function (tex, texUnit) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imageData);
withProgram('copy', viewportQuadVertex, copyTexFragment, function (ref) {
var setUniform = ref.setUniform;
var setAttribute = ref.setAttribute;
setAttribute('aUV', 2, gl.STATIC_DRAW, 0, new Float32Array([0, 0, 2, 0, 0, 2]));
setUniform('1i', 'image', texUnit);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer || null);
gl.disable(gl.BLEND);
gl.colorMask(channels & 8, channels & 4, channels & 2, channels & 1);
gl.viewport(x, y, width, height);
gl.scissor(x, y, width, height);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
});
});
}
/**
* Resizing a canvas clears its contents; this utility copies the previous contents over.
* @param canvas
* @param newWidth
* @param newHeight
*/
function resizeWebGLCanvasWithoutClearing(canvas, newWidth, newHeight) {
var width = canvas.width;
var height = canvas.height;
withWebGLContext(canvas, function (ref) {
var gl = ref.gl;
var data = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
canvas.width = newWidth;
canvas.height = newHeight;
renderImageData(gl, data, 0, 0, width, height);
});
}
var webglUtils = /*#__PURE__*/Object.freeze({
__proto__: null,
withWebGLContext: withWebGLContext,
renderImageData: renderImageData,
resizeWebGLCanvasWithoutClearing: resizeWebGLCanvasWithoutClearing
});
function generate$2 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
var textureData = new Uint8Array(sdfWidth * sdfHeight);
var viewBoxWidth = viewBox[2] - viewBox[0];
var viewBoxHeight = viewBox[3] - viewBox[1];
// Decompose all paths into straight line segments and add them to an index
var segments = [];
pathToLineSegments(path, function (x1, y1, x2, y2) {
segments.push({
x1: x1, y1: y1, x2: x2, y2: y2,
minX: Math.min(x1, x2),
minY: Math.min(y1, y2),
maxX: Math.max(x1, x2),
maxY: Math.max(y1, y2)
});
});
// Sort segments by maxX, this will let us short-circuit some loops below
segments.sort(function (a, b) { return a.maxX - b.maxX; });
// For each target SDF texel, find the distance from its center to its nearest line segment,
// map that distance to an alpha value, and write that alpha to the texel
for (var sdfX = 0; sdfX < sdfWidth; sdfX++) {
for (var sdfY = 0; sdfY < sdfHeight; sdfY++) {
var signedDist = findNearestSignedDistance(
viewBox[0] + viewBoxWidth * (sdfX + 0.5) / sdfWidth,
viewBox[1] + viewBoxHeight * (sdfY + 0.5) / sdfHeight
);
// Use an exponential scale to ensure the texels very near the glyph path have adequate
// precision, while allowing the distance field to cover the entire texture, given that
// there are only 8 bits available. Formula visualized: https://www.desmos.com/calculator/uiaq5aqiam
var alpha = Math.pow((1 - Math.abs(signedDist) / maxDistance), sdfExponent) / 2;
if (signedDist < 0) {
alpha = 1 - alpha;
}
alpha = Math.max(0, Math.min(255, Math.round(alpha * 255))); //clamp
textureData[sdfY * sdfWidth + sdfX] = alpha;
}
}
return textureData
/**
* For a given x/y, search the index for the closest line segment and return
* its signed distance. Negative = inside, positive = outside, zero = on edge
* @param x
* @param y
* @returns {number}
*/
function findNearestSignedDistance (x, y) {
var closestDistSq = Infinity;
var closestDist = Infinity;
for (var i = segments.length; i--;) {
var seg = segments[i];
if (seg.maxX + closestDist <= x) { break } //sorting by maxX means no more can be closer, so we can short-circuit
if (x + closestDist > seg.minX && y - closestDist < seg.maxY && y + closestDist > seg.minY) {
var distSq = absSquareDistanceToLineSegment(x, y, seg.x1, seg.y1, seg.x2, seg.y2);
if (distSq < closestDistSq) {
closestDistSq = distSq;
closestDist = Math.sqrt(closestDistSq);
}
}
}
// Flip to negative distance if inside the poly
if (isPointInPoly(x, y)) {
closestDist = -closestDist;
}
return closestDist
}
/**
* Determine whether the given point lies inside or outside the glyph. Uses a simple
* winding-number ray casting algorithm using a ray pointing east from the point.
*/
function isPointInPoly (x, y) {
var winding = 0;
for (var i = segments.length; i--;) {
var seg = segments[i];
if (seg.maxX <= x) { break } //sorting by maxX means no more can cross, so we can short-circuit
var intersects = ((seg.y1 > y) !== (seg.y2 > y)) && (x < (seg.x2 - seg.x1) * (y - seg.y1) / (seg.y2 - seg.y1) + seg.x1);
if (intersects) {
winding += seg.y1 < seg.y2 ? 1 : -1;
}
}
return winding !== 0
}
}
function generateIntoCanvas$2(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
generateIntoFramebuffer$1(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, null, x, y, channel);
}
function generateIntoFramebuffer$1 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas, framebuffer, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
var data = generate$2(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent);
// Expand single-channel data to rbga
var rgbaData = new Uint8Array(data.length * 4);
for (var i = 0; i < data.length; i++) {
rgbaData[i * 4 + channel] = data[i];
}
renderImageData(glOrCanvas, rgbaData, x, y, sdfWidth, sdfHeight, 1 << (3 - channel), framebuffer);
}
/**
* Find the absolute distance from a point to a line segment at closest approach
*/
function absSquareDistanceToLineSegment (x, y, lineX0, lineY0, lineX1, lineY1) {
var ldx = lineX1 - lineX0;
var ldy = lineY1 - lineY0;
var lengthSq = ldx * ldx + ldy * ldy;
var t = lengthSq ? Math.max(0, Math.min(1, ((x - lineX0) * ldx + (y - lineY0) * ldy) / lengthSq)) : 0;
var dx = x - (lineX0 + t * ldx);
var dy = y - (lineY0 + t * ldy);
return dx * dx + dy * dy
}
var javascript = /*#__PURE__*/Object.freeze({
__proto__: null,
generate: generate$2,
generateIntoCanvas: generateIntoCanvas$2,
generateIntoFramebuffer: generateIntoFramebuffer$1
});
var mainVertex = "precision highp float;uniform vec4 uGlyphBounds;attribute vec2 aUV;attribute vec4 aLineSegment;varying vec4 vLineSegment;varying vec2 vGlyphXY;void main(){vLineSegment=aLineSegment;vGlyphXY=mix(uGlyphBounds.xy,uGlyphBounds.zw,aUV);gl_Position=vec4(mix(vec2(-1.0),vec2(1.0),aUV),0.0,1.0);}";
var mainFragment = "precision highp float;uniform vec4 uGlyphBounds;uniform float uMaxDistance;uniform float uExponent;varying vec4 vLineSegment;varying vec2 vGlyphXY;float absDistToSegment(vec2 point,vec2 lineA,vec2 lineB){vec2 lineDir=lineB-lineA;float lenSq=dot(lineDir,lineDir);float t=lenSq==0.0 ? 0.0 : clamp(dot(point-lineA,lineDir)/lenSq,0.0,1.0);vec2 linePt=lineA+t*lineDir;return distance(point,linePt);}void main(){vec4 seg=vLineSegment;vec2 p=vGlyphXY;float dist=absDistToSegment(p,seg.xy,seg.zw);float val=pow(1.0-clamp(dist/uMaxDistance,0.0,1.0),uExponent)*0.5;bool crossing=(seg.y>p.y!=seg.w>p.y)&&(p.x<(seg.z-seg.x)*(p.y-seg.y)/(seg.w-seg.y)+seg.x);bool crossingUp=crossing&&vLineSegment.y<vLineSegment.w;gl_FragColor=vec4(crossingUp ? 1.0/255.0 : 0.0,crossing&&!crossingUp ? 1.0/255.0 : 0.0,0.0,val);}";
var postFragment = "precision highp float;uniform sampler2D tex;varying vec2 vUV;void main(){vec4 color=texture2D(tex,vUV);bool inside=color.r!=color.g;float val=inside ? 1.0-color.a : color.a;gl_FragColor=vec4(val);}";
// Single triangle covering viewport
var viewportUVs = new Float32Array([0, 0, 2, 0, 0, 2]);
var implicitContext = null;
var isTestingSupport = false;
var NULL_OBJECT = {};
var supportByCanvas = new WeakMap(); // canvas -> bool
function validateSupport (glOrCanvas) {
if (!isTestingSupport && !isSupported(glOrCanvas)) {
throw new Error('WebGL generation not supported')
}
}
function generate$1 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( glOrCanvas === void 0 ) glOrCanvas = null;
if (!glOrCanvas) {
glOrCanvas = implicitContext;
if (!glOrCanvas) {
var canvas = typeof OffscreenCanvas === 'function'
? new OffscreenCanvas(1, 1)
: typeof document !== 'undefined'
? document.createElement('canvas')
: null;
if (!canvas) {
throw new Error('OffscreenCanvas or DOM canvas not supported')
}
glOrCanvas = implicitContext = canvas.getContext('webgl', { depth: false });
}
}
validateSupport(glOrCanvas);
var rgbaData = new Uint8Array(sdfWidth * sdfHeight * 4); //not Uint8ClampedArray, cuz Safari
// Render into a background texture framebuffer
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var withTexture = ref.withTexture;
var withTextureFramebuffer = ref.withTextureFramebuffer;
withTexture('readable', function (texture, textureUnit) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, sdfWidth, sdfHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
withTextureFramebuffer(texture, textureUnit, function (framebuffer) {
generateIntoFramebuffer(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent,
gl,
framebuffer,
0,
0,
0 // red channel
);
gl.readPixels(0, 0, sdfWidth, sdfHeight, gl.RGBA, gl.UNSIGNED_BYTE, rgbaData);
});
});
});
// Throw away all but the red channel
var data = new Uint8Array(sdfWidth * sdfHeight);
for (var i = 0, j = 0; i < rgbaData.length; i += 4) {
data[j++] = rgbaData[i];
}
return data
}
function generateIntoCanvas$1(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
generateIntoFramebuffer(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, null, x, y, channel);
}
function generateIntoFramebuffer (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas, framebuffer, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
// Verify support
validateSupport(glOrCanvas);
// Compute path segments
var lineSegmentCoords = [];
pathToLineSegments(path, function (x1, y1, x2, y2) {
lineSegmentCoords.push(x1, y1, x2, y2);
});
lineSegmentCoords = new Float32Array(lineSegmentCoords);
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var isWebGL2 = ref.isWebGL2;
var getExtension = ref.getExtension;
var withProgram = ref.withProgram;
var withTexture = ref.withTexture;
var withTextureFramebuffer = ref.withTextureFramebuffer;
var handleContextLoss = ref.handleContextLoss;
withTexture('rawDistances', function (intermediateTexture, intermediateTextureUnit) {
if (sdfWidth !== intermediateTexture._lastWidth || sdfHeight !== intermediateTexture._lastHeight) {
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA,
intermediateTexture._lastWidth = sdfWidth,
intermediateTexture._lastHeight = sdfHeight,
0, gl.RGBA, gl.UNSIGNED_BYTE, null
);
}
// Unsigned distance pass
withProgram('main', mainVertex, mainFragment, function (ref) {
var setAttribute = ref.setAttribute;
var setUniform = ref.setUniform;
// Init extensions
var instancingExtension = !isWebGL2 && getExtension('ANGLE_instanced_arrays');
var blendMinMaxExtension = !isWebGL2 && getExtension('EXT_blend_minmax');
// Init/update attributes
setAttribute('aUV', 2, gl.STATIC_DRAW, 0, viewportUVs);
setAttribute('aLineSegment', 4, gl.DYNAMIC_DRAW, 1, lineSegmentCoords);
// Init/update uniforms
setUniform.apply(void 0, [ '4f', 'uGlyphBounds' ].concat( viewBox ));
setUniform('1f', 'uMaxDistance', maxDistance);
setUniform('1f', 'uExponent', sdfExponent);
// Render initial unsigned distance / winding number info to a texture
withTextureFramebuffer(intermediateTexture, intermediateTextureUnit, function (framebuffer) {
gl.enable(gl.BLEND);
gl.colorMask(true, true, true, true);
gl.viewport(0, 0, sdfWidth, sdfHeight);
gl.scissor(0, 0, sdfWidth, sdfHeight);
gl.blendFunc(gl.ONE, gl.ONE);
// Red+Green channels are incremented (FUNC_ADD) for segment-ray crossings to give a "winding number".
// Alpha holds the closest (MAX) unsigned distance.
gl.blendEquationSeparate(gl.FUNC_ADD, isWebGL2 ? gl.MAX : blendMinMaxExtension.MAX_EXT);
gl.clear(gl.COLOR_BUFFER_BIT);
if (isWebGL2) {
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, lineSegmentCoords.length / 4);
} else {
instancingExtension.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 3, lineSegmentCoords.length / 4);
}
// Debug
// const debug = new Uint8Array(sdfWidth * sdfHeight * 4)
// gl.readPixels(0, 0, sdfWidth, sdfHeight, gl.RGBA, gl.UNSIGNED_BYTE, debug)
// console.log('intermediate texture data: ', debug)
});
});
// Use the data stored in the texture to apply inside/outside and write to the output framebuffer rect+channel.
withProgram('post', viewportQuadVertex, postFragment, function (program) {
program.setAttribute('aUV', 2, gl.STATIC_DRAW, 0, viewportUVs);
program.setUniform('1i', 'tex', intermediateTextureUnit);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.disable(gl.BLEND);
gl.colorMask(channel === 0, channel === 1, channel === 2, channel === 3);
gl.viewport(x, y, sdfWidth, sdfHeight);
gl.scissor(x, y, sdfWidth, sdfHeight);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
});
// Handle context loss occurring during any of the above calls
if (gl.isContextLost()) {
handleContextLoss();
throw new Error('webgl context lost')
}
});
}
function isSupported (glOrCanvas) {
var key = (!glOrCanvas || glOrCanvas === implicitContext) ? NULL_OBJECT : (glOrCanvas.canvas || glOrCanvas);
var supported = supportByCanvas.get(key);
if (supported === undefined) {
isTestingSupport = true;
var failReason = null;
try {
// Since we can't detect all failure modes up front, let's just do a trial run of a
// simple path and compare what we get back to the correct expected result. This will
// also serve to prime the shader compilation.
var expectedResult = [
97, 106, 97, 61,
99, 137, 118, 80,
80, 118, 137, 99,
61, 97, 106, 97
];
var testResult = generate$1(
4,
4,
'M8,8L16,8L24,24L16,24Z',
[0, 0, 32, 32],
24,
1,
glOrCanvas
);
supported = testResult && expectedResult.length === testResult.length &&
testResult.every(function (val, i) { return val === expectedResult[i]; });
if (!supported) {
failReason = 'bad trial run results';
console.info(expectedResult, testResult);
}
} catch (err) {
// TODO if it threw due to webgl context loss, should we maybe leave isSupported as null and try again later?
supported = false;
failReason = err.message;
}
if (failReason) {
console.warn('WebGL SDF generation not supported:', failReason);
}
isTestingSupport = false;
supportByCanvas.set(key, supported);
}
return supported
}
var webgl = /*#__PURE__*/Object.freeze({
__proto__: null,
generate: generate$1,
generateIntoCanvas: generateIntoCanvas$1,
generateIntoFramebuffer: generateIntoFramebuffer,
isSupported: isSupported
});
/**
* Generate an SDF texture image for a 2D path.
*
* @param {number} sdfWidth - width of the SDF output image in pixels.
* @param {number} sdfHeight - height of the SDF output image in pixels.
* @param {string} path - an SVG-like path string describing the glyph; should only contain commands: M/L/Q/C/Z.
* @param {number[]} viewBox - [minX, minY, maxX, maxY] in font units aligning with the texture's edges.
* @param {number} maxDistance - the maximum distance from the glyph path in font units that will be encoded; defaults
* to half the maximum viewBox dimension.
* @param {number} [sdfExponent] - specifies an exponent for encoding the SDF's distance values; higher exponents
* will give greater precision nearer the glyph's path.
* @return {Uint8Array}
*/
function generate(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent
) {
if ( maxDistance === void 0 ) maxDistance = Math.max(viewBox[2] - viewBox[0], viewBox[3] - viewBox[1]) / 2;
if ( sdfExponent === void 0 ) sdfExponent = 1;
try {
return generate$1.apply(webgl, arguments)
} catch(e) {
console.info('WebGL SDF generation failed, falling back to JS', e);
return generate$2.apply(javascript, arguments)
}
}
/**
* Generate an SDF texture image for a 2D path, inserting the result into a WebGL `canvas` at a given x/y position
* and color channel. This is generally much faster than calling `generate` because it does not require reading pixels
* back from the GPU->CPU -- the `canvas` can be used directly as a WebGL texture image, so it all stays on the GPU.
*
* @param {number} sdfWidth - width of the SDF output image in pixels.
* @param {number} sdfHeight - height of the SDF output image in pixels.
* @param {string} path - an SVG-like path string describing the glyph; should only contain commands: M/L/Q/C/Z.
* @param {number[]} viewBox - [minX, minY, maxX, maxY] in font units aligning with the texture's edges.
* @param {number} maxDistance - the maximum distance from the glyph path in font units that will be encoded; defaults
* to half the maximum viewBox dimension.
* @param {number} [sdfExponent] - specifies an exponent for encoding the SDF's distance values; higher exponents
* will give greater precision nearer the glyph's path.
* @param {HTMLCanvasElement|OffscreenCanvas} canvas - a WebGL-enabled canvas into which the SDF will be rendered.
* Only the relevant rect/channel will be modified, the rest will be preserved. To avoid unpredictable results
* due to shared GL context state, this canvas should be dedicated to use by this library alone.
* @param {number} x - the x position at which to render the SDF.
* @param {number} y - the y position at which to render the SDF.
* @param {number} channel - the color channel index (0-4) into which the SDF will be rendered.
* @return {Uint8Array}
*/
function generateIntoCanvas(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent,
canvas,
x,
y,
channel
) {
if ( maxDistance === void 0 ) maxDistance = Math.max(viewBox[2] - viewBox[0], viewBox[3] - viewBox[1]) / 2;
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
try {
return generateIntoCanvas$1.apply(webgl, arguments)
} catch(e) {
console.info('WebGL SDF generation failed, falling back to JS', e);
return generateIntoCanvas$2.apply(javascript, arguments)
}
}
exports.forEachPathCommand = forEachPathCommand;
exports.generate = generate;
exports.generateIntoCanvas = generateIntoCanvas;
exports.javascript = javascript;
exports.pathToLineSegments = pathToLineSegments;
exports.webgl = webgl;
exports.webglUtils = webglUtils;
Object.defineProperty(exports, '__esModule', { value: true });
return exports;
}({}));
return exports
}
return SDFGenerator;
})));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+822
View File
@@ -0,0 +1,822 @@
function SDFGenerator() {
var exports = (function (exports) {
/**
* Find the point on a quadratic bezier curve at t where t is in the range [0, 1]
*/
function pointOnQuadraticBezier (x0, y0, x1, y1, x2, y2, t, pointOut) {
var t2 = 1 - t;
pointOut.x = t2 * t2 * x0 + 2 * t2 * t * x1 + t * t * x2;
pointOut.y = t2 * t2 * y0 + 2 * t2 * t * y1 + t * t * y2;
}
/**
* Find the point on a cubic bezier curve at t where t is in the range [0, 1]
*/
function pointOnCubicBezier (x0, y0, x1, y1, x2, y2, x3, y3, t, pointOut) {
var t2 = 1 - t;
pointOut.x = t2 * t2 * t2 * x0 + 3 * t2 * t2 * t * x1 + 3 * t2 * t * t * x2 + t * t * t * x3;
pointOut.y = t2 * t2 * t2 * y0 + 3 * t2 * t2 * t * y1 + 3 * t2 * t * t * y2 + t * t * t * y3;
}
/**
* Parse a path string into its constituent line/curve commands, invoking a callback for each.
* @param {string} pathString - An SVG-like path string to parse; should only contain commands: M/L/Q/C/Z
* @param {function(
* command: 'L'|'Q'|'C',
* startX: number,
* startY: number,
* endX: number,
* endY: number,
* ctrl1X?: number,
* ctrl1Y?: number,
* ctrl2X?: number,
* ctrl2Y?: number
* )} commandCallback - A callback function that will be called once for each parsed path command, passing the
* command identifier (only L/Q/C commands) and its numeric arguments.
*/
function forEachPathCommand(pathString, commandCallback) {
var segmentRE = /([MLQCZ])([^MLQCZ]*)/g;
var match, firstX, firstY, prevX, prevY;
while ((match = segmentRE.exec(pathString))) {
var args = match[2]
.replace(/^\s*|\s*$/g, '')
.split(/[,\s]+/)
.map(function (v) { return parseFloat(v); });
switch (match[1]) {
case 'M':
prevX = firstX = args[0];
prevY = firstY = args[1];
break
case 'L':
if (args[0] !== prevX || args[1] !== prevY) { // yup, some fonts have zero-length line commands
commandCallback('L', prevX, prevY, (prevX = args[0]), (prevY = args[1]));
}
break
case 'Q': {
commandCallback('Q', prevX, prevY, (prevX = args[2]), (prevY = args[3]), args[0], args[1]);
break
}
case 'C': {
commandCallback('C', prevX, prevY, (prevX = args[4]), (prevY = args[5]), args[0], args[1], args[2], args[3]);
break
}
case 'Z':
if (prevX !== firstX || prevY !== firstY) {
commandCallback('L', prevX, prevY, firstX, firstY);
}
break
}
}
}
/**
* Convert a path string to a series of straight line segments
* @param {string} pathString - An SVG-like path string to parse; should only contain commands: M/L/Q/C/Z
* @param {function(x1:number, y1:number, x2:number, y2:number)} segmentCallback - A callback
* function that will be called once for every line segment
* @param {number} [curvePoints] - How many straight line segments to use when approximating a
* bezier curve in the path. Defaults to 16.
*/
function pathToLineSegments (pathString, segmentCallback, curvePoints) {
if ( curvePoints === void 0 ) curvePoints = 16;
var tempPoint = { x: 0, y: 0 };
forEachPathCommand(pathString, function (command, startX, startY, endX, endY, ctrl1X, ctrl1Y, ctrl2X, ctrl2Y) {
switch (command) {
case 'L':
segmentCallback(startX, startY, endX, endY);
break
case 'Q': {
var prevCurveX = startX;
var prevCurveY = startY;
for (var i = 1; i < curvePoints; i++) {
pointOnQuadraticBezier(
startX, startY,
ctrl1X, ctrl1Y,
endX, endY,
i / (curvePoints - 1),
tempPoint
);
segmentCallback(prevCurveX, prevCurveY, tempPoint.x, tempPoint.y);
prevCurveX = tempPoint.x;
prevCurveY = tempPoint.y;
}
break
}
case 'C': {
var prevCurveX$1 = startX;
var prevCurveY$1 = startY;
for (var i$1 = 1; i$1 < curvePoints; i$1++) {
pointOnCubicBezier(
startX, startY,
ctrl1X, ctrl1Y,
ctrl2X, ctrl2Y,
endX, endY,
i$1 / (curvePoints - 1),
tempPoint
);
segmentCallback(prevCurveX$1, prevCurveY$1, tempPoint.x, tempPoint.y);
prevCurveX$1 = tempPoint.x;
prevCurveY$1 = tempPoint.y;
}
break
}
}
});
}
var viewportQuadVertex = "precision highp float;attribute vec2 aUV;varying vec2 vUV;void main(){vUV=aUV;gl_Position=vec4(mix(vec2(-1.0),vec2(1.0),aUV),0.0,1.0);}";
var copyTexFragment = "precision highp float;uniform sampler2D tex;varying vec2 vUV;void main(){gl_FragColor=texture2D(tex,vUV);}";
var cache = new WeakMap();
var glContextParams = {
premultipliedAlpha: false,
preserveDrawingBuffer: true,
antialias: false,
depth: false,
};
/**
* This is a little helper library for WebGL. It assists with state management for a GL context.
* It's pretty tightly wrapped to the needs of this package, not very general-purpose.
*
* @param { WebGLRenderingContext | HTMLCanvasElement | OffscreenCanvas } glOrCanvas - the GL context to wrap
* @param { ({gl, getExtension, withProgram, withTexture, withTextureFramebuffer, handleContextLoss}) => void } callback
*/
function withWebGLContext (glOrCanvas, callback) {
var gl = glOrCanvas.getContext ? glOrCanvas.getContext('webgl', glContextParams) : glOrCanvas;
var wrapper = cache.get(gl);
if (!wrapper) {
var isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
var extensions = {};
var programs = {};
var textures = {};
var textureUnit = -1;
var framebufferStack = [];
gl.canvas.addEventListener('webglcontextlost', function (e) {
handleContextLoss();
e.preventDefault();
}, false);
function getExtension (name) {
var ext = extensions[name];
if (!ext) {
ext = extensions[name] = gl.getExtension(name);
if (!ext) {
throw new Error((name + " not supported"))
}
}
return ext
}
function compileShader (src, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
// const status = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
// if (!status && !gl.isContextLost()) {
// throw new Error(gl.getShaderInfoLog(shader).trim())
// }
return shader
}
function withProgram (name, vert, frag, func) {
if (!programs[name]) {
var attributes = {};
var uniforms = {};
var program = gl.createProgram();
gl.attachShader(program, compileShader(vert, gl.VERTEX_SHADER));
gl.attachShader(program, compileShader(frag, gl.FRAGMENT_SHADER));
gl.linkProgram(program);
programs[name] = {
program: program,
transaction: function transaction (func) {
gl.useProgram(program);
func({
setUniform: function setUniform (type, name) {
var values = [], len = arguments.length - 2;
while ( len-- > 0 ) values[ len ] = arguments[ len + 2 ];
var uniformLoc = uniforms[name] || (uniforms[name] = gl.getUniformLocation(program, name));
gl[("uniform" + type)].apply(gl, [ uniformLoc ].concat( values ));
},
setAttribute: function setAttribute (name, size, usage, instancingDivisor, data) {
var attr = attributes[name];
if (!attr) {
attr = attributes[name] = {
buf: gl.createBuffer(), // TODO should we destroy our buffers?
loc: gl.getAttribLocation(program, name),
data: null
};
}
gl.bindBuffer(gl.ARRAY_BUFFER, attr.buf);
gl.vertexAttribPointer(attr.loc, size, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(attr.loc);
if (isWebGL2) {
gl.vertexAttribDivisor(attr.loc, instancingDivisor);
} else {
getExtension('ANGLE_instanced_arrays').vertexAttribDivisorANGLE(attr.loc, instancingDivisor);
}
if (data !== attr.data) {
gl.bufferData(gl.ARRAY_BUFFER, data, usage);
attr.data = data;
}
}
});
}
};
}
programs[name].transaction(func);
}
function withTexture (name, func) {
textureUnit++;
try {
gl.activeTexture(gl.TEXTURE0 + textureUnit);
var texture = textures[name];
if (!texture) {
texture = textures[name] = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
}
gl.bindTexture(gl.TEXTURE_2D, texture);
func(texture, textureUnit);
} finally {
textureUnit--;
}
}
function withTextureFramebuffer (texture, textureUnit, func) {
var framebuffer = gl.createFramebuffer();
framebufferStack.push(framebuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
try {
func(framebuffer);
} finally {
gl.deleteFramebuffer(framebuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebufferStack[--framebufferStack.length - 1] || null);
}
}
function handleContextLoss () {
extensions = {};
programs = {};
textures = {};
textureUnit = -1;
framebufferStack.length = 0;
}
cache.set(gl, wrapper = {
gl: gl,
isWebGL2: isWebGL2,
getExtension: getExtension,
withProgram: withProgram,
withTexture: withTexture,
withTextureFramebuffer: withTextureFramebuffer,
handleContextLoss: handleContextLoss,
});
}
callback(wrapper);
}
function renderImageData(glOrCanvas, imageData, x, y, width, height, channels, framebuffer) {
if ( channels === void 0 ) channels = 15;
if ( framebuffer === void 0 ) framebuffer = null;
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var withProgram = ref.withProgram;
var withTexture = ref.withTexture;
withTexture('copy', function (tex, texUnit) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imageData);
withProgram('copy', viewportQuadVertex, copyTexFragment, function (ref) {
var setUniform = ref.setUniform;
var setAttribute = ref.setAttribute;
setAttribute('aUV', 2, gl.STATIC_DRAW, 0, new Float32Array([0, 0, 2, 0, 0, 2]));
setUniform('1i', 'image', texUnit);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer || null);
gl.disable(gl.BLEND);
gl.colorMask(channels & 8, channels & 4, channels & 2, channels & 1);
gl.viewport(x, y, width, height);
gl.scissor(x, y, width, height);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
});
});
}
/**
* Resizing a canvas clears its contents; this utility copies the previous contents over.
* @param canvas
* @param newWidth
* @param newHeight
*/
function resizeWebGLCanvasWithoutClearing(canvas, newWidth, newHeight) {
var width = canvas.width;
var height = canvas.height;
withWebGLContext(canvas, function (ref) {
var gl = ref.gl;
var data = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
canvas.width = newWidth;
canvas.height = newHeight;
renderImageData(gl, data, 0, 0, width, height);
});
}
var webglUtils = /*#__PURE__*/Object.freeze({
__proto__: null,
withWebGLContext: withWebGLContext,
renderImageData: renderImageData,
resizeWebGLCanvasWithoutClearing: resizeWebGLCanvasWithoutClearing
});
function generate$2 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
var textureData = new Uint8Array(sdfWidth * sdfHeight);
var viewBoxWidth = viewBox[2] - viewBox[0];
var viewBoxHeight = viewBox[3] - viewBox[1];
// Decompose all paths into straight line segments and add them to an index
var segments = [];
pathToLineSegments(path, function (x1, y1, x2, y2) {
segments.push({
x1: x1, y1: y1, x2: x2, y2: y2,
minX: Math.min(x1, x2),
minY: Math.min(y1, y2),
maxX: Math.max(x1, x2),
maxY: Math.max(y1, y2)
});
});
// Sort segments by maxX, this will let us short-circuit some loops below
segments.sort(function (a, b) { return a.maxX - b.maxX; });
// For each target SDF texel, find the distance from its center to its nearest line segment,
// map that distance to an alpha value, and write that alpha to the texel
for (var sdfX = 0; sdfX < sdfWidth; sdfX++) {
for (var sdfY = 0; sdfY < sdfHeight; sdfY++) {
var signedDist = findNearestSignedDistance(
viewBox[0] + viewBoxWidth * (sdfX + 0.5) / sdfWidth,
viewBox[1] + viewBoxHeight * (sdfY + 0.5) / sdfHeight
);
// Use an exponential scale to ensure the texels very near the glyph path have adequate
// precision, while allowing the distance field to cover the entire texture, given that
// there are only 8 bits available. Formula visualized: https://www.desmos.com/calculator/uiaq5aqiam
var alpha = Math.pow((1 - Math.abs(signedDist) / maxDistance), sdfExponent) / 2;
if (signedDist < 0) {
alpha = 1 - alpha;
}
alpha = Math.max(0, Math.min(255, Math.round(alpha * 255))); //clamp
textureData[sdfY * sdfWidth + sdfX] = alpha;
}
}
return textureData
/**
* For a given x/y, search the index for the closest line segment and return
* its signed distance. Negative = inside, positive = outside, zero = on edge
* @param x
* @param y
* @returns {number}
*/
function findNearestSignedDistance (x, y) {
var closestDistSq = Infinity;
var closestDist = Infinity;
for (var i = segments.length; i--;) {
var seg = segments[i];
if (seg.maxX + closestDist <= x) { break } //sorting by maxX means no more can be closer, so we can short-circuit
if (x + closestDist > seg.minX && y - closestDist < seg.maxY && y + closestDist > seg.minY) {
var distSq = absSquareDistanceToLineSegment(x, y, seg.x1, seg.y1, seg.x2, seg.y2);
if (distSq < closestDistSq) {
closestDistSq = distSq;
closestDist = Math.sqrt(closestDistSq);
}
}
}
// Flip to negative distance if inside the poly
if (isPointInPoly(x, y)) {
closestDist = -closestDist;
}
return closestDist
}
/**
* Determine whether the given point lies inside or outside the glyph. Uses a simple
* winding-number ray casting algorithm using a ray pointing east from the point.
*/
function isPointInPoly (x, y) {
var winding = 0;
for (var i = segments.length; i--;) {
var seg = segments[i];
if (seg.maxX <= x) { break } //sorting by maxX means no more can cross, so we can short-circuit
var intersects = ((seg.y1 > y) !== (seg.y2 > y)) && (x < (seg.x2 - seg.x1) * (y - seg.y1) / (seg.y2 - seg.y1) + seg.x1);
if (intersects) {
winding += seg.y1 < seg.y2 ? 1 : -1;
}
}
return winding !== 0
}
}
function generateIntoCanvas$2(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
generateIntoFramebuffer$1(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, null, x, y, channel);
}
function generateIntoFramebuffer$1 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas, framebuffer, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
var data = generate$2(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent);
// Expand single-channel data to rbga
var rgbaData = new Uint8Array(data.length * 4);
for (var i = 0; i < data.length; i++) {
rgbaData[i * 4 + channel] = data[i];
}
renderImageData(glOrCanvas, rgbaData, x, y, sdfWidth, sdfHeight, 1 << (3 - channel), framebuffer);
}
/**
* Find the absolute distance from a point to a line segment at closest approach
*/
function absSquareDistanceToLineSegment (x, y, lineX0, lineY0, lineX1, lineY1) {
var ldx = lineX1 - lineX0;
var ldy = lineY1 - lineY0;
var lengthSq = ldx * ldx + ldy * ldy;
var t = lengthSq ? Math.max(0, Math.min(1, ((x - lineX0) * ldx + (y - lineY0) * ldy) / lengthSq)) : 0;
var dx = x - (lineX0 + t * ldx);
var dy = y - (lineY0 + t * ldy);
return dx * dx + dy * dy
}
var javascript = /*#__PURE__*/Object.freeze({
__proto__: null,
generate: generate$2,
generateIntoCanvas: generateIntoCanvas$2,
generateIntoFramebuffer: generateIntoFramebuffer$1
});
var mainVertex = "precision highp float;uniform vec4 uGlyphBounds;attribute vec2 aUV;attribute vec4 aLineSegment;varying vec4 vLineSegment;varying vec2 vGlyphXY;void main(){vLineSegment=aLineSegment;vGlyphXY=mix(uGlyphBounds.xy,uGlyphBounds.zw,aUV);gl_Position=vec4(mix(vec2(-1.0),vec2(1.0),aUV),0.0,1.0);}";
var mainFragment = "precision highp float;uniform vec4 uGlyphBounds;uniform float uMaxDistance;uniform float uExponent;varying vec4 vLineSegment;varying vec2 vGlyphXY;float absDistToSegment(vec2 point,vec2 lineA,vec2 lineB){vec2 lineDir=lineB-lineA;float lenSq=dot(lineDir,lineDir);float t=lenSq==0.0 ? 0.0 : clamp(dot(point-lineA,lineDir)/lenSq,0.0,1.0);vec2 linePt=lineA+t*lineDir;return distance(point,linePt);}void main(){vec4 seg=vLineSegment;vec2 p=vGlyphXY;float dist=absDistToSegment(p,seg.xy,seg.zw);float val=pow(1.0-clamp(dist/uMaxDistance,0.0,1.0),uExponent)*0.5;bool crossing=(seg.y>p.y!=seg.w>p.y)&&(p.x<(seg.z-seg.x)*(p.y-seg.y)/(seg.w-seg.y)+seg.x);bool crossingUp=crossing&&vLineSegment.y<vLineSegment.w;gl_FragColor=vec4(crossingUp ? 1.0/255.0 : 0.0,crossing&&!crossingUp ? 1.0/255.0 : 0.0,0.0,val);}";
var postFragment = "precision highp float;uniform sampler2D tex;varying vec2 vUV;void main(){vec4 color=texture2D(tex,vUV);bool inside=color.r!=color.g;float val=inside ? 1.0-color.a : color.a;gl_FragColor=vec4(val);}";
// Single triangle covering viewport
var viewportUVs = new Float32Array([0, 0, 2, 0, 0, 2]);
var implicitContext = null;
var isTestingSupport = false;
var NULL_OBJECT = {};
var supportByCanvas = new WeakMap(); // canvas -> bool
function validateSupport (glOrCanvas) {
if (!isTestingSupport && !isSupported(glOrCanvas)) {
throw new Error('WebGL generation not supported')
}
}
function generate$1 (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( glOrCanvas === void 0 ) glOrCanvas = null;
if (!glOrCanvas) {
glOrCanvas = implicitContext;
if (!glOrCanvas) {
var canvas = typeof OffscreenCanvas === 'function'
? new OffscreenCanvas(1, 1)
: typeof document !== 'undefined'
? document.createElement('canvas')
: null;
if (!canvas) {
throw new Error('OffscreenCanvas or DOM canvas not supported')
}
glOrCanvas = implicitContext = canvas.getContext('webgl', { depth: false });
}
}
validateSupport(glOrCanvas);
var rgbaData = new Uint8Array(sdfWidth * sdfHeight * 4); //not Uint8ClampedArray, cuz Safari
// Render into a background texture framebuffer
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var withTexture = ref.withTexture;
var withTextureFramebuffer = ref.withTextureFramebuffer;
withTexture('readable', function (texture, textureUnit) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, sdfWidth, sdfHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
withTextureFramebuffer(texture, textureUnit, function (framebuffer) {
generateIntoFramebuffer(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent,
gl,
framebuffer,
0,
0,
0 // red channel
);
gl.readPixels(0, 0, sdfWidth, sdfHeight, gl.RGBA, gl.UNSIGNED_BYTE, rgbaData);
});
});
});
// Throw away all but the red channel
var data = new Uint8Array(sdfWidth * sdfHeight);
for (var i = 0, j = 0; i < rgbaData.length; i += 4) {
data[j++] = rgbaData[i];
}
return data
}
function generateIntoCanvas$1(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
generateIntoFramebuffer(sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, canvas, null, x, y, channel);
}
function generateIntoFramebuffer (sdfWidth, sdfHeight, path, viewBox, maxDistance, sdfExponent, glOrCanvas, framebuffer, x, y, channel) {
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
// Verify support
validateSupport(glOrCanvas);
// Compute path segments
var lineSegmentCoords = [];
pathToLineSegments(path, function (x1, y1, x2, y2) {
lineSegmentCoords.push(x1, y1, x2, y2);
});
lineSegmentCoords = new Float32Array(lineSegmentCoords);
withWebGLContext(glOrCanvas, function (ref) {
var gl = ref.gl;
var isWebGL2 = ref.isWebGL2;
var getExtension = ref.getExtension;
var withProgram = ref.withProgram;
var withTexture = ref.withTexture;
var withTextureFramebuffer = ref.withTextureFramebuffer;
var handleContextLoss = ref.handleContextLoss;
withTexture('rawDistances', function (intermediateTexture, intermediateTextureUnit) {
if (sdfWidth !== intermediateTexture._lastWidth || sdfHeight !== intermediateTexture._lastHeight) {
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA,
intermediateTexture._lastWidth = sdfWidth,
intermediateTexture._lastHeight = sdfHeight,
0, gl.RGBA, gl.UNSIGNED_BYTE, null
);
}
// Unsigned distance pass
withProgram('main', mainVertex, mainFragment, function (ref) {
var setAttribute = ref.setAttribute;
var setUniform = ref.setUniform;
// Init extensions
var instancingExtension = !isWebGL2 && getExtension('ANGLE_instanced_arrays');
var blendMinMaxExtension = !isWebGL2 && getExtension('EXT_blend_minmax');
// Init/update attributes
setAttribute('aUV', 2, gl.STATIC_DRAW, 0, viewportUVs);
setAttribute('aLineSegment', 4, gl.DYNAMIC_DRAW, 1, lineSegmentCoords);
// Init/update uniforms
setUniform.apply(void 0, [ '4f', 'uGlyphBounds' ].concat( viewBox ));
setUniform('1f', 'uMaxDistance', maxDistance);
setUniform('1f', 'uExponent', sdfExponent);
// Render initial unsigned distance / winding number info to a texture
withTextureFramebuffer(intermediateTexture, intermediateTextureUnit, function (framebuffer) {
gl.enable(gl.BLEND);
gl.colorMask(true, true, true, true);
gl.viewport(0, 0, sdfWidth, sdfHeight);
gl.scissor(0, 0, sdfWidth, sdfHeight);
gl.blendFunc(gl.ONE, gl.ONE);
// Red+Green channels are incremented (FUNC_ADD) for segment-ray crossings to give a "winding number".
// Alpha holds the closest (MAX) unsigned distance.
gl.blendEquationSeparate(gl.FUNC_ADD, isWebGL2 ? gl.MAX : blendMinMaxExtension.MAX_EXT);
gl.clear(gl.COLOR_BUFFER_BIT);
if (isWebGL2) {
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, lineSegmentCoords.length / 4);
} else {
instancingExtension.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 3, lineSegmentCoords.length / 4);
}
// Debug
// const debug = new Uint8Array(sdfWidth * sdfHeight * 4)
// gl.readPixels(0, 0, sdfWidth, sdfHeight, gl.RGBA, gl.UNSIGNED_BYTE, debug)
// console.log('intermediate texture data: ', debug)
});
});
// Use the data stored in the texture to apply inside/outside and write to the output framebuffer rect+channel.
withProgram('post', viewportQuadVertex, postFragment, function (program) {
program.setAttribute('aUV', 2, gl.STATIC_DRAW, 0, viewportUVs);
program.setUniform('1i', 'tex', intermediateTextureUnit);
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.disable(gl.BLEND);
gl.colorMask(channel === 0, channel === 1, channel === 2, channel === 3);
gl.viewport(x, y, sdfWidth, sdfHeight);
gl.scissor(x, y, sdfWidth, sdfHeight);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
});
// Handle context loss occurring during any of the above calls
if (gl.isContextLost()) {
handleContextLoss();
throw new Error('webgl context lost')
}
});
}
function isSupported (glOrCanvas) {
var key = (!glOrCanvas || glOrCanvas === implicitContext) ? NULL_OBJECT : (glOrCanvas.canvas || glOrCanvas);
var supported = supportByCanvas.get(key);
if (supported === undefined) {
isTestingSupport = true;
var failReason = null;
try {
// Since we can't detect all failure modes up front, let's just do a trial run of a
// simple path and compare what we get back to the correct expected result. This will
// also serve to prime the shader compilation.
var expectedResult = [
97, 106, 97, 61,
99, 137, 118, 80,
80, 118, 137, 99,
61, 97, 106, 97
];
var testResult = generate$1(
4,
4,
'M8,8L16,8L24,24L16,24Z',
[0, 0, 32, 32],
24,
1,
glOrCanvas
);
supported = testResult && expectedResult.length === testResult.length &&
testResult.every(function (val, i) { return val === expectedResult[i]; });
if (!supported) {
failReason = 'bad trial run results';
console.info(expectedResult, testResult);
}
} catch (err) {
// TODO if it threw due to webgl context loss, should we maybe leave isSupported as null and try again later?
supported = false;
failReason = err.message;
}
if (failReason) {
console.warn('WebGL SDF generation not supported:', failReason);
}
isTestingSupport = false;
supportByCanvas.set(key, supported);
}
return supported
}
var webgl = /*#__PURE__*/Object.freeze({
__proto__: null,
generate: generate$1,
generateIntoCanvas: generateIntoCanvas$1,
generateIntoFramebuffer: generateIntoFramebuffer,
isSupported: isSupported
});
/**
* Generate an SDF texture image for a 2D path.
*
* @param {number} sdfWidth - width of the SDF output image in pixels.
* @param {number} sdfHeight - height of the SDF output image in pixels.
* @param {string} path - an SVG-like path string describing the glyph; should only contain commands: M/L/Q/C/Z.
* @param {number[]} viewBox - [minX, minY, maxX, maxY] in font units aligning with the texture's edges.
* @param {number} maxDistance - the maximum distance from the glyph path in font units that will be encoded; defaults
* to half the maximum viewBox dimension.
* @param {number} [sdfExponent] - specifies an exponent for encoding the SDF's distance values; higher exponents
* will give greater precision nearer the glyph's path.
* @return {Uint8Array}
*/
function generate(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent
) {
if ( maxDistance === void 0 ) maxDistance = Math.max(viewBox[2] - viewBox[0], viewBox[3] - viewBox[1]) / 2;
if ( sdfExponent === void 0 ) sdfExponent = 1;
try {
return generate$1.apply(webgl, arguments)
} catch(e) {
console.info('WebGL SDF generation failed, falling back to JS', e);
return generate$2.apply(javascript, arguments)
}
}
/**
* Generate an SDF texture image for a 2D path, inserting the result into a WebGL `canvas` at a given x/y position
* and color channel. This is generally much faster than calling `generate` because it does not require reading pixels
* back from the GPU->CPU -- the `canvas` can be used directly as a WebGL texture image, so it all stays on the GPU.
*
* @param {number} sdfWidth - width of the SDF output image in pixels.
* @param {number} sdfHeight - height of the SDF output image in pixels.
* @param {string} path - an SVG-like path string describing the glyph; should only contain commands: M/L/Q/C/Z.
* @param {number[]} viewBox - [minX, minY, maxX, maxY] in font units aligning with the texture's edges.
* @param {number} maxDistance - the maximum distance from the glyph path in font units that will be encoded; defaults
* to half the maximum viewBox dimension.
* @param {number} [sdfExponent] - specifies an exponent for encoding the SDF's distance values; higher exponents
* will give greater precision nearer the glyph's path.
* @param {HTMLCanvasElement|OffscreenCanvas} canvas - a WebGL-enabled canvas into which the SDF will be rendered.
* Only the relevant rect/channel will be modified, the rest will be preserved. To avoid unpredictable results
* due to shared GL context state, this canvas should be dedicated to use by this library alone.
* @param {number} x - the x position at which to render the SDF.
* @param {number} y - the y position at which to render the SDF.
* @param {number} channel - the color channel index (0-4) into which the SDF will be rendered.
* @return {Uint8Array}
*/
function generateIntoCanvas(
sdfWidth,
sdfHeight,
path,
viewBox,
maxDistance,
sdfExponent,
canvas,
x,
y,
channel
) {
if ( maxDistance === void 0 ) maxDistance = Math.max(viewBox[2] - viewBox[0], viewBox[3] - viewBox[1]) / 2;
if ( sdfExponent === void 0 ) sdfExponent = 1;
if ( x === void 0 ) x = 0;
if ( y === void 0 ) y = 0;
if ( channel === void 0 ) channel = 0;
try {
return generateIntoCanvas$1.apply(webgl, arguments)
} catch(e) {
console.info('WebGL SDF generation failed, falling back to JS', e);
return generateIntoCanvas$2.apply(javascript, arguments)
}
}
exports.forEachPathCommand = forEachPathCommand;
exports.generate = generate;
exports.generateIntoCanvas = generateIntoCanvas;
exports.javascript = javascript;
exports.pathToLineSegments = pathToLineSegments;
exports.webgl = webgl;
exports.webglUtils = webglUtils;
Object.defineProperty(exports, '__esModule', { value: true });
return exports;
}({}));
return exports
}
export { SDFGenerator as default };
+33
View File
@@ -0,0 +1,33 @@
{
"name": "webgl-sdf-generator",
"version": "1.1.1",
"description": "WebGL-accelerated signed distance field generation for 2D paths",
"main": "dist/webgl-sdf-generator.js",
"module": "dist/webgl-sdf-generator.mjs",
"repository": {
"type": "git",
"url": "https://github.com/lojjic/webgl-sdf-generator.git"
},
"scripts": {
"build": "rollup -c rollup.config.js",
"test": "npm run build && node test/run.js"
},
"author": "Jason Johnston",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/node": "^7.16.0",
"@babel/plugin-transform-modules-commonjs": "^7.16.0",
"@rollup/plugin-buble": "^0.21.3",
"event-target-polyfill": "0.0.3",
"gl": "^4.9.2",
"rollup": "^2.56.3",
"rollup-plugin-glsl": "^1.3.0",
"rollup-plugin-terser": "^7.0.2"
},
"files": [
"/dist",
"/LICENSE.txt",
"/README.md"
]
}