(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 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; })));