UNPKG

p5

Version:

[![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)

1,603 lines 52.9 kB
import { textCoreConstants } from './textCore.js';
import { t as CENTER, aa as RIGHT, s as RADIUS } from '../constants-DwbuOBz3.js';
import { UnicodeRange } from '@japont/unicode-range';
import { unicodeRanges } from './unicodeRanges.js';
import { Vector } from '../math/p5.Vector.js';
import Typr from './lib/Typr.js';
import { createFromCommands } from '@davepagurek/bezier-path';
import '../p5.Renderer-N-APumjv.js';
import '../creating_reading-Be7_6X4p.js';
import 'colorjs.io/fn';
import '../color/color_spaces/hsb.js';
import '../image/filters.js';
import '../shape/custom_shapes.js';
import '../core/States.js';
import '../io/utilities.js';
import 'pako';

/**
 * @module Typography
 */


const pathArgCounts = { M: 2, L: 2, C: 6, Q: 4 };
const validFontTypes = ['ttf', 'otf', 'woff'];//, 'woff2'];
const validFontTypesRe = new RegExp(`\\.(${validFontTypes.join('|')})`, 'i');
const extractFontNameRe = new RegExp(`([^/]+)(\\.(?:${validFontTypes.join('|')}))`, 'i');
const invalidFontError = 'Sorry, only TTF, OTF and WOFF files are supported.'; // and WOFF2
const fontFaceVariations = ['weight', 'stretch', 'style'];


class Font {
  constructor(p, fontFace, name, path, data) {
    if (!(fontFace instanceof FontFace)) {
      throw Error('FontFace is required');
    }
    this._pInst = p;
    this.name = name;
    this.path = path;
    this.data = data;
    this.face = fontFace;
  }

  /**
   * Checks whether a font has glyph point data and
   * can thus be used for textToPoints(), WEBGL mode, etc.
   * @private
   */
  static hasGlyphData(textFont) {
    let { font } = textFont;
    return typeof font === 'object' && typeof font.data !== 'undefined';
  }

  fontBounds(str, x, y, width, height, options) {
    ({ width, height, options } = this._parseArgs(width, height, options));
    let renderer = options?.graphics?._renderer || this._pInst._renderer;
    if (!renderer) throw Error('p5 or graphics required for fontBounds()');
    return renderer.fontBounds(str, x, y, width, height);
  }

  textBounds(str, x, y, width, height, options) {
    ({ width, height, options } = this._parseArgs(width, height, options));
    let renderer = options?.graphics?._renderer || this._pInst._renderer;
    if (!renderer) throw Error('p5 or graphics required for fontBounds()');
    return renderer.textBounds(str, x, y, width, height);
  }

  /**
   * Returns a flat array of path commands that describe the outlines of a string of text.
   *
   * Each command is represented as an array of the form `[type, ...coords]`, where:
   * - `type` is one of `'M'`, `'L'`, `'Q'`, `'C'`, or `'Z'`,
   * - `coords` are the numeric values needed for that command.
   *
   * `'M'` indicates a "move to" (starting a new contour),
   * `'L'` a line segment,
   * `'Q'` a quadratic bezier,
   * `'C'` a cubic bezier, and
   * `'Z'` closes the current path.
   *
   * The first two parameters, `x` and `y`, specify the baseline origin for the text.
   * Optionally, you can provide a `width` and `height` for text wrapping; if you don't need
   * wrapping, you can omit them and directly pass `options` as the fourth parameter.
   *
   * @param  {String} str            The text to convert into path commands.
   * @param  {Number} x              x‐coordinate of the text baseline.
   * @param  {Number} y              y‐coordinate of the text baseline.
   * @param  {Number} [width]        Optional width for text wrapping.
   * @param  {Number} [height]       Optional height for text wrapping.
   * @return {Array<Array>}          A flat array of path commands.
   *
   * @example
   * let font;
   *
   * async function setup() {
   *   font = await loadFont('assets/inconsolata.otf');
   *   createCanvas(200, 200);
   *   background(220);
   *   noLoop();
   * }
   *
   * function draw() {
   *   background(220);
   *   stroke(0);
   *   noFill();
   *   textSize(60);
   *
   *   // Get path commands for "Hello" (drawn at baseline x=50, y=100):
   *   const pathCommands = font.textToPaths('Hello', 30, 110);
   *
   *   beginShape();
   *   for (let i = 0; i < pathCommands.length; i++) {
   *     const cmd = pathCommands[i];
   *     const type = cmd[0];
   *
   *     switch (type) {
   *       case 'M': {
   *         // Move to (start a new contour)
   *         const x = cmd[1];
   *         const y = cmd[2];
   *         endContour(); // In case we were already drawing
   *         beginContour();
   *         vertex(x, y);
   *         break;
   *       }
   *       case 'L': {
   *         // Line to
   *         const x = cmd[1];
   *         const y = cmd[2];
   *         vertex(x, y);
   *         break;
   *       }
   *       case 'Q': {
   *         // Quadratic bezier
   *         const cx = cmd[1];
   *         const cy = cmd[2];
   *         const x = cmd[3];
   *         const y = cmd[4];
   *         bezierOrder(2);
   *         bezierVertex(cx, cy);
   *         bezierVertex(x, y);
   *         break;
   *       }
   *       case 'C': {
   *         // Cubic bezier
   *         const cx1 = cmd[1];
   *         const cy1 = cmd[2];
   *         const cx2 = cmd[3];
   *         const cy2 = cmd[4];
   *         const x = cmd[5];
   *         const y = cmd[6];
   *         bezierOrder(3);
   *         bezierVertex(cx1, cy1);
   *         bezierVertex(cx2, cy2);
   *         bezierVertex(x, y);
   *         break;
   *       }
   *       case 'Z': {
   *         // Close path
   *         endContour(CLOSE);
   *         beginContour();
   *         break;
   *       }
   *     }
   *   }
   *   endContour();
   *   endShape();
   * }
   */
  textToPaths(str, x, y, width, height, options) {

    ({ width, height, options } = this._parseArgs(width, height, options));

    if (!this.data) {
      throw Error('No font data available for "' + this.name
        + '"\nTry downloading a local copy of the font file');
    }

    // lineate and get glyphs/paths for each line
    let lines = this._lineateAndPathify(str, x, y, width, height, options);

    // flatten into a single array containing all the glyphs
    let glyphs = lines.map(o => o.glyphs).flat();

    // flatten into a single array with all the path commands
    return glyphs.map(g => g.path.commands).flat();
  }

  /**
   * Returns an array of points outlining a string of text written using the
   * font.
   *
   * Each point object in the array has three properties that describe the
   * point's location and orientation, called its path angle. For example,
   * `{ x: 10, y: 20, alpha: 450 }`.
   *
   * The first parameter, `str`, is a string of text. The second and third
   * parameters, `x` and `y`, are the text's position. By default, they set the
   * coordinates of the bounding box's bottom-left corner. See
   * <a href="#/p5/textAlign">textAlign()</a> for more ways to align text.
   *
   * The fourth parameter, `options`, is also optional. `font.textToPoints()`
   * expects an object with the following properties:
   *
   * `sampleFactor` is the ratio of the text's path length to the number of
   * samples. It defaults to 0.1. Higher values produce more points along the
   * path and are more precise.
   *
   * `simplifyThreshold` removes collinear points if it's set to a number other
   * than 0. The value represents the threshold angle in radians to use when determining
   * whether two edges are collinear.
   *
   * @param  {String} str        string of text.
   * @param  {Number} x          x-coordinate of the text.
   * @param  {Number} y          y-coordinate of the text.
   * @param  {Object} [options]  Configuration:
   * @param  {Number} [options.sampleFactor=0.1] The ratio of the text's path length to the number of samples.
   * @param  {Number} [options.simplifyThreshold=0] A minmum angle in radian sbetween two segments. Segments with a shallower angle will be merged.
   * @return {Array<Object>} array of point objects, each with `x`, `y`, and `alpha` (path angle) properties.
   *
   * @example
   * let font;
   *
   * async function setup() {
   *   createCanvas(100, 100);
   *   font = await loadFont('assets/inconsolata.otf');
   *
   *   background(200);
   *   textSize(35);
   *
   *   // Get the point array.
   *   let points = font.textToPoints('p5*js', 6, 60, { sampleFactor: 0.5 });
   *
   *   // Draw a dot at each point.
   *   for (let p of points) {
   *     point(p.x, p.y);
   *   }
   *
   *   describe('A set of black dots outlining the text "p5*js" on a gray background.');
   * }
   */
  textToPoints(str, x, y, width, height, options) {
    // By segmenting per contour, pointAtLength becomes much faster
    const contourPoints = this.textToContours(
      str,
      x, y,
      width, height,
      options
    );
    return contourPoints.reduce((acc, next) => {
      acc.push(...next);
      return acc;
    }, []);
  }

  /**
   * Returns an array of arrays of points outlining a string of text written using the
   * font. Each array represents a contour, so the letter O will have two outer arrays:
   * one for the outer edge of the shape, and one for the inner edge of the hole.
   *
   * Each point object in a contour array has three properties that describe the
   * point's location and orientation, called its path angle. For example,
   * `{ x: 10, y: 20, alpha: 450 }`.
   *
   * The first parameter, `str`, is a string of text. The second and third
   * parameters, `x` and `y`, are the text's position. By default, they set the
   * coordinates of the bounding box's bottom-left corner. See
   * <a href="#/p5/textAlign">textAlign()</a> for more ways to align text.
   *
   * The fourth parameter, `options`, is also optional. `font.textToContours()`
   * expects an object with the following properties:
   *
   * `sampleFactor` is the ratio of the text's path length to the number of
   * samples. It defaults to 0.1. Higher values produce more points along the
   * path and are more precise.
   *
   * `simplifyThreshold` removes collinear points if it's set to a number other
   * than 0. The value represents the threshold angle in radians to use when determining
   * whether two edges are collinear.
   *
   * @param  {String} str        string of text.
   * @param  {Number} x          x-coordinate of the text.
   * @param  {Number} y          y-coordinate of the text.
   * @param  {Object} [options]  Configuration options:
   * @param  {Number} [options.sampleFactor=0.1] The ratio of the text's path length to the number of samples.
   * @param  {Number} [options.simplifyThreshold=0] A minmum angle in radians between two segments. Segments with a shallower angle will be merged.
   * @return {Array<Array<Object>>} array of point objects, each with `x`, `y`, and `alpha` (path angle) properties.
   *
   * @example
   * let font;
   *
   * async function setup() {
   *   createCanvas(100, 100);
   *   font = await loadFont('/assets/inconsolata.otf');
   * }
   *
   * function draw() {
   *   background(200);
   *   textAlign(CENTER, CENTER);
   *   textSize(30);
   *
   *   // Get the point array.
   *   let contours = font.textToContours('p5*js', width/2, height/2, { sampleFactor: 0.5 });
   *
   *   beginShape();
   *   for (const pts of contours) {
   *     beginContour();
   *     for (const pt of pts) {
   *       vertex(pt.x + 5*sin(pt.y*0.1 + millis()*0.01), pt.y);
   *     }
   *     endContour(CLOSE);
   *   }
   *   endShape();
   *
   *   describe('The text p5*js wobbling over time');
   * }
   */
  textToContours(str, x = 0, y = 0, width, height, options) {
    ({ width, height, options } = this._parseArgs(width, height, options));

    const cmds = this.textToPaths(str, x, y, width, height, options);
    const cmdContours = [];
    for (const cmd of cmds) {
      if (cmd[0] === 'M') {
        cmdContours.push([]);
      }
      cmdContours[cmdContours.length - 1].push(cmd);
    }

    return cmdContours.map(commands => pathToPoints(commands, options, this));
  }
  /**
   *
   * Converts text into a 3D model that can be rendered in WebGL mode.
   *
   * This method transforms flat text into extruded 3D geometry, allowing
   * for dynamic effects like depth, warping, and custom shading.
   *
   * It works by taking the outlines (contours) of each character in the
   * provided text string and constructing a 3D shape from them.
   *
   * Once your 3D text is ready, you can rotate it in 3D space using <a href="#/p5/orbitControl">orbitControl()</a>
   * — just click and drag with your mouse to see it from all angles!
   *
   * Use the extrude slider to give your letters depth: slide it up, and your
   * flat text turns into a solid, multi-dimensional object.
   *
   * You can also choose from various fonts such as "Anton", "Montserrat", or "Source Serif",
   * much like selecting fancy fonts in a word processor,
   *
   * The generated model (a Geometry object) can be manipulated further—rotated, scaled,
   * or styled with shaders—to create engaging, interactive visual art.
   * 
   * The `options` parameter is also optional. `font.textToModel()` expects an object 
   * with the following properties:
   * 
   * `extrude` is the depth to extrude the text. It defaults to 0. A value of 0 produces
   * flat text; higher values create thicker, 3D models.
   * 
   * `sampleFactor` is a factor controlling the level of detail for the text contours.
   * It defaults to 1. Higher values result in smoother curves.
   *
   * @param {String} str The text string to convert into a 3D model.
   * @param {Number} x The x-coordinate for the starting position of the text.
   * @param {Number} y The y-coordinate for the starting position of the text.
   * @param {Number} width Maximum width of the text block (wraps text if exceeded).
   * @param {Number} height Maximum height of the text block.
   * @param {Object} [options] Configuration options for the 3D text:
   * @param {Number} [options.extrude=0] The depth to extrude the text. A value of 0 produces
   * flat text; higher values create thicker, 3D models.
   * @param {Number} [options.sampleFactor=1] A factor controlling the level of detail for the text contours.
   *  Higher values result in smoother curves.
   * @return {p5.Geometry} A geometry object representing the 3D model of the text.
   *
   * @example
   * let font;
   * let geom;
   *
   * async function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   font = await loadFont('https://fonts.gstatic.com/s/anton/v25/1Ptgg87LROyAm0K08i4gS7lu.ttf');
   *
   *   geom = font.textToModel("Hello", 50, 0, { sampleFactor: 2 });
   *   geom.clearColors();
   *   geom.normalize();
   * }
   *
   * function draw() {
   *   background(255);
   *   orbitControl();
   *   fill("red");
   *   strokeWeight(4);
   *   scale(min(width, height) / 300);
   *   model(geom);
   *   describe('A red non-extruded "Hello" in Anton on white canvas, rotatable via mouse.');
   * }
   *
   * @example
   * let font;
   * let geom;
   *
   * async function setup() {
   *   createCanvas(200, 200, WEBGL);
   *
   *   // Alternative fonts:
   *   // Anton: 'https://fonts.gstatic.com/s/anton/v25/1Ptgg87LROyAm0K08i4gS7lu.ttf'
   *   // Montserrat: 'https://fonts.gstatic.com/s/montserrat/v29/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-Y3tcoqK5.ttf'
   *   // Source Serif: 'https://fonts.gstatic.com/s/sourceserif4/v8/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqrhxXD-wGvjU.ttf'
   *
   *   // Using Source Serif for this example:
   *   font = await loadFont('https://fonts.gstatic.com/s/sourceserif4/v8/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqrhxXD-wGvjU.ttf');
   *
   *   geom = font.textToModel("Hello", 50, 0, { sampleFactor: 2, extrude: 5 });
   *   geom.clearColors();
   *   geom.normalize();
   * }
   *
   * function draw() {
   *   background(255);
   *   orbitControl();
   *   fill("red");
   *   strokeWeight(4);
   *   scale(min(width, height) / 300);
   *   model(geom);
   *   describe('3D red extruded "Hello" in Source Serif on white, rotatable via mouse.');
   * }
   *
   * @example
   * let geom;
   * let activeFont;
   * let artShader;
   * let lineShader;
   *
   * // Define parameters as simple variables
   * let words = 'HELLO';
   * let warp = 1;
   * let extrude = 5;
   * let palette = ["#ffe03d", "#fe4830", "#d33033", "#6d358a", "#1c509e", "#00953c"];
   *
   * async function setup() {
   *   createCanvas(200, 200, WEBGL);
   *
   *   // Using Anton as the default font for this example:
   *
   *  // Alternative fonts:
   *  // Anton: 'https://fonts.gstatic.com/s/anton/v25/1Ptgg87LROyAm0K08i4gS7lu.ttf'
   *  // Montserrat: 'https://fonts.gstatic.com/s/montserrat/v29/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Ew-Y3tcoqK5.ttf'
   *  // Source Serif: 'https://fonts.gstatic.com/s/sourceserif4/v8/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqrhxXD-wGvjU.ttf'
   *   activeFont = await loadFont('https://fonts.gstatic.com/s/anton/v25/1Ptgg87LROyAm0K08i4gS7lu.ttf');
   *
   *   geom = activeFont.textToModel(words, 0, 50, { sampleFactor: 2, extrude });
   *   geom.clearColors();
   *   geom.normalize();
   *
   *   artShader = baseMaterialShader().modify({
   *     uniforms: {
   *       'float time': () => millis(),
   *       'float warp': () => warp,
   *       'float numColors': () => palette.length,
   *       'vec3[6] colors': () => palette.flatMap((c) => [red(c)/255, green(c)/255, blue(c)/255]),
   *     },
   *     vertexDeclarations: 'out vec3 vPos;',
   *     fragmentDeclarations: 'in vec3 vPos;',
   *     'Vertex getObjectInputs': `(Vertex inputs) {
   *       vPos = inputs.position;
   *       inputs.position.x += 5. * warp * sin(inputs.position.y * 0.1 + time * 0.001) / (1. + warp);
   *       inputs.position.y += 5. * warp * sin(inputs.position.x * 0.1 + time * 0.0009) / (1. + warp);
   *       return inputs;
   *     }`,
   *     'vec4 getFinalColor': `(vec4 _c) {
   *       float x = vPos.x * 0.005;
   *       float a = floor(fract(x) * numColors);
   *       float b = a == numColors - 1. ? 0. : a + 1.;
   *       float t = fract(x * numColors);
   *       vec3 c = mix(colors[int(a)], colors[int(b)], t);
   *       return vec4(c, 1.);
   *     }`
   *   });
   *
   *   lineShader = baseStrokeShader().modify({
   *     uniforms: {
   *       'float time': () => millis(),
   *       'float warp': () => warp,
   *     },
   *     'StrokeVertex getObjectInputs': `(StrokeVertex inputs) {
   *       inputs.position.x += 5. * warp * sin(inputs.position.y * 0.1 + time * 0.001) / (1. + warp);
   *       inputs.position.y += 5. * warp * sin(inputs.position.x * 0.1 + time * 0.0009) / (1. + warp);
   *       return inputs;
   *     }`,
   *   });
   * }
   *
   * function draw() {
   *   background(255);
   *   orbitControl();
   *   shader(artShader);
   *   strokeShader(lineShader);
   *   strokeWeight(4);
   *   scale(min(width, height) / 210);
   *   model(geom);
   *   describe('3D wavy with different color sets "Hello" in Anton on white canvas, rotatable via mouse.');
   * }
   */
  textToModel(str, x, y, width, height, options) {
    ({ width, height, options } = this._parseArgs(width, height, options));
    const extrude = options?.extrude || 0;

    let contours = this.textToContours(str, x, y, width, height, options);
    if (!contours || contours.length === 0) {
      return new p5.Geometry();
    }

    // Step 2: build base flat geometry - single shape
    const geom = this._pInst.buildGeometry(() => {
      const prevValidateFaces = this._pInst._renderer._validateFaces;
      this._pInst._renderer._validateFaces = true;

      this._pInst.beginShape();
      for (const contour of contours) {
        this._pInst.beginContour();
        for (const pt of contour) {
          this._pInst.vertex(pt.x, pt.y, 0);
        }
        this._pInst.endContour(this._pInst.CLOSE);
      }

      this._pInst.endShape(this._pInst.CLOSE);

      this._pInst._renderer._validateFaces = prevValidateFaces;
    });

    if (extrude === 0) {
      return geom;
    }

    // The tessellation process creates separate vertices for each triangle,
    // even when they share the same position. We need to deduplicate them
    // to find which faces are actually connected, so we can identify the
    // outer edges for extrusion.

    const vertexIndices = {};
    const vertexId = v => `${v.x.toFixed(6)}-${v.y.toFixed(6)}-${v.z.toFixed(6)}`;
    const newVertices = [];
    const newVertexIndex = [];

    for (const v of geom.vertices) {
      const id = vertexId(v);
      if (!(id in vertexIndices)) {
        const index = newVertices.length;
        vertexIndices[id] = index;
        newVertices.push(v.copy());
      }
      newVertexIndex.push(vertexIndices[id]);
    }

    // Remap faces to use deduplicated vertices
    const newFaces = geom.faces.map(f => f.map(i => newVertexIndex[i]));

    //Find outer edges (edges that appear in only one face)
    const seen = {};
    for (const face of newFaces) {
      for (let off = 0; off < face.length; off++) {
        const a = face[off];
        const b = face[(off + 1) % face.length];
        const id = `${Math.min(a, b)}-${Math.max(a, b)}`;
        if (!seen[id]) seen[id] = [];
        seen[id].push([a, b]);
      }
    }
    const validEdges = [];
    for (const key in seen) {
      if (seen[key].length === 1) {
        validEdges.push(seen[key][0]);
      }
    }

    // Step 5: Create extruded geometry
    const extruded = this._pInst.buildGeometry(() => {});
    const half = extrude * 0.5;
    extruded.vertices = [];
    extruded.faces = [];
    extruded.edges = []; // INITIALIZE EDGES ARRAY

    // Add side face vertices (separate for each edge for flat shading)
    for (const [a, b] of validEdges) {
      const vA = newVertices[a];
      const vB = newVertices[b];

      // Skip if vertices are too close (degenerate edge)
      // We only need to check the perimeter edge length since the other edge
      // is the extrude direction, which is always > 0 for extruded geometry

      const edgeVector = new Vector(vB.x - vA.x, vB.y - vA.y, vB.z - vA.z);
      const extrudeVector = new Vector(0, 0, extrude);
      const crossProduct = Vector.cross(edgeVector, extrudeVector);
      const dist = edgeVector.mag();
      if (crossProduct.mag() < 0.0001 || dist < 0.0001) continue;
      // Front face vertices
      const frontA = extruded.vertices.length;
      extruded.vertices.push(new Vector(vA.x, vA.y, vA.z + half));
      const frontB = extruded.vertices.length;
      extruded.vertices.push(new Vector(vB.x, vB.y, vB.z + half));
      const backA = extruded.vertices.length;
      extruded.vertices.push(new Vector(vA.x, vA.y, vA.z - half));
      const backB = extruded.vertices.length;
      extruded.vertices.push(new Vector(vB.x, vB.y, vB.z - half));

      extruded.faces.push([frontA, backA, backB]);
      extruded.faces.push([frontA, backB, frontB]);
      extruded.edges.push([frontA, frontB]);
      extruded.edges.push([backA, backB]);
      extruded.edges.push([frontA, backA]);
      extruded.edges.push([frontB, backB]);
    }

    // Add front face (with unshared vertices for flat shading)
    const frontVertexOffset = extruded.vertices.length;
    for (const v of newVertices) {
      extruded.vertices.push(new Vector(v.x, v.y, v.z + half));
    }
    for (const face of newFaces) {
      if (face.length < 3) continue;
      const mappedFace = face.map(i => i + frontVertexOffset);
      extruded.faces.push(mappedFace);

      // ADD EDGES FOR FRONT FACE
      for (let i = 0; i < mappedFace.length; i++) {
        const nextIndex = (i + 1) % mappedFace.length;
        extruded.edges.push([mappedFace[i], mappedFace[nextIndex]]);
      }
    }

    // Add back face (reversed winding order)
    const backVertexOffset = extruded.vertices.length;
    for (const v of newVertices) {
      extruded.vertices.push(new Vector(v.x, v.y, v.z - half));
    }

    for (const face of newFaces) {
      if (face.length < 3) continue;
      const mappedFace = [...face].reverse().map(i => i + backVertexOffset);
      extruded.faces.push(mappedFace);

      // ADD EDGES FOR BACK FACE
      for (let i = 0; i < mappedFace.length; i++) {
        const nextIndex = (i + 1) % mappedFace.length;
        extruded.edges.push([mappedFace[i], mappedFace[nextIndex]]);
      }
    }

    extruded.computeNormals();
    return extruded;
  }

  variations() {
    let vars = {};
    if (this.data) {
      let axes = this.face?.axes;
      if (axes) {
        axes.forEach(ax => {
          vars[ax.tag] = ax.value;
        });
      }
    }
    fontFaceVariations.forEach(v => {
      let val = this.face[v];
      if (val !== 'normal') {
        vars[v] = vars[v] || val;
      }
    });
    return vars;
  }

  metadata() {
    let meta = this.data?.name || {};
    for (let p in this.face) {
      if (!/^load/.test(p)) {
        meta[p] = meta[p] || this.face[p];
      }
    }
    return meta;
  }

  static async list(log = false) { // tmp
    if (log) {
      console.log('There are', document.fonts.size, 'font-faces\n');
      let loaded = 0;
      for (let fontFace of document.fonts.values()) {
        console.log('FontFace: {');
        for (let property in fontFace) {
          console.log('  ' + property + ': ' + fontFace[property]);
        }
        console.log('}\n');
        if (fontFace.status === 'loaded') {
          loaded++;
        }
      }
      console.log(loaded + ' loaded');
    }
    return await Array.from(document.fonts);
  }

  /////////////////////////////// HELPERS ////////////////////////////////

  /*
    Returns an array of line objects, each containing { text, x, y, glyphs: [ {g, path} ] }
  */
  _lineateAndPathify(str, x, y, width, height, options = {}) {

    let renderer = options?.graphics?._renderer || this._pInst._renderer;
    renderer.push();
    renderer.textFont(this);

    // lineate and compute bounds for the text
    let { lines, bounds } = renderer._computeBounds
    (textCoreConstants._FONT_BOUNDS, str, x, y, width, height,
      { ignoreRectMode: true, ...options });

    // compute positions for each of the lines
    lines = this._position(renderer, lines, bounds, width, height);

    // convert lines to paths
    let uPE = this.data?.head?.unitsPerEm || 1000;
    let scale = renderer.states.textSize / uPE;

    const axs = this._currentAxes(renderer);
    let pathsForLine = lines.map(l => this._lineToGlyphs(l, { scale, axs }));

    renderer.pop();

    return pathsForLine;
  }

  _currentAxes(renderer) {
    let axs;
    if ((this.data?.fvar?.length ?? 0) > 0) {
      const fontAxes = this.data.fvar[0];
      axs = fontAxes.map(([tag, minVal, defaultVal, maxVal, flags, name]) => {
        if (!renderer) return defaultVal;
        if (tag === 'wght') {
          return renderer.states.fontWeight;
        } else if (tag === 'wdth') {
          // TODO: map from keywords (normal, ultra-condensed, etc) to values
          // return renderer.states.fontStretch
          return 100;
        } else if (renderer.textCanvas().style.fontVariationSettings) {
          const match = new RegExp(`\\b${tag}\s+(\d+)`)
            .exec(renderer.textCanvas().style.fontVariationSettings);
          if (match) {
            return parseInt(match[1]);
          } else {
            return defaultVal;
          }
        } else {
          return defaultVal;
        }
      });
    }
    return axs;
  }

  _textToPathPoints(str, x, y, width, height, options) {

    ({ width, height, options } = this._parseArgs(width, height, options));

    // lineate and get the points for each line
    let cmds = this.textToPaths(str, x, y, width, height, options);

    // divide line-segments with intermediate points
    const subdivide = (pts, pt1, pt2, md) => {
      if (fn.dist(pt1.x, pt1.y, pt2.x, pt2.y) > md) {
        let middle = { x: (pt1.x + pt2.x) / 2, y: (pt1.y + pt2.y) / 2 };
        pts.push(middle);
        subdivide(pts, pt1, middle, md);
        subdivide(pts, middle, pt2, md);
      }
    };

    // a point for each path-command plus line subdivisions
    let pts = [];
    let { textSize } = this._pInst._renderer.states;
    let maxDist = (textSize / this.data.head.unitsPerEm) * 500;

    for (let i = 0; i < cmds.length; i++) {
      let { type, data: d } = cmds[i];
      if (type !== 'Z') {
        let pt = { x: d[d.length - 2], y: d[d.length - 1] };
        if (type === 'L' && pts.length && !options?.nodivide > 0) {
          subdivide(pts, pts[pts.length - 1], pt, maxDist);
        }
        pts.push(pt);
      }
    }

    return pts;
  }

  _parseArgs(width, height, options = {}) {

    if (typeof width === 'object') {
      options = width;
      width = height = undefined;
    }
    else if (typeof height === 'object') {
      options = height;
      height = undefined;
    }
    return { width, height, options };
  }

  _position(renderer, lines, bounds, width, height) {

    let { textAlign, textLeading, textSize } = renderer.states;
    let metrics = this._measureTextDefault(renderer, 'X');
    let ascent = metrics.fontBoundingBoxAscent;

    let coordify = (text, i) => {
      let x = bounds.x;
      let y = bounds.y + (i * textLeading) + ascent;
      let lineWidth = renderer._fontWidthSingle(text);
      if (textAlign === CENTER) {
        x += (bounds.w - lineWidth) / 2;
      }
      else if (textAlign === RIGHT) {
        x += (bounds.w - lineWidth);
      }
      if (typeof width !== 'undefined') {
        switch (renderer.states.rectMode) {
          case CENTER:
            x -= width / 2;
            y -= height / 2;
            break;
          case RADIUS:
            x -= width;
            y -= height;
            break;
        }
      }
      return { text, x, y };
    };

    return lines.map(coordify);
  }

  _lineToGlyphs(line, { scale = 1, axs } = {}) {

    if (!this.data) {
      throw Error('No font data available for "' + this.name
        + '"\nTry downloading a local copy of the font file');
    }
    let glyphShapes = Typr.U.shape(this.data, line.text, { axs });
    line.glyphShapes = glyphShapes;

    line.glyphs = this._shapeToPaths(glyphShapes, line, { scale, axs });

    return line;
  }

  _positionGlyphs(text, options) {
    let renderer = options?.graphics?._renderer || this._pInst._renderer;
    const axs = this._currentAxes(renderer);
    const glyphShapes = Typr.U.shape(this.data, text, { axs });
    const positionedGlyphs = [];
    let x = 0;
    for (const glyph of glyphShapes) {
      positionedGlyphs.push({ x, index: glyph.g, shape: glyph });
      x += glyph.ax;
    }
    return positionedGlyphs;
  }

  _singleShapeToPath(shape, {
    scale = 1,
    x = 0,
    y = 0,
    lineX = 0,
    lineY = 0,
    axs
  } = {}) {
    let font = this.data;
    let crdIdx = 0;
    let { g, ax, ay, dx, dy } = shape;
    let { crds, cmds } = Typr.U.glyphToPath(font, g, true, axs);

    // can get simple points for each glyph here, but we don't need them ?
    let glyph = { /*g: line.text[i], points: [],*/ path: { commands: [] } };

    for (let j = 0; j < cmds.length; j++) {
      let type = cmds[j], command = [type];
      if (type in pathArgCounts) {
        let argCount = pathArgCounts[type];
        for (let k = 0; k < argCount; k += 2) {
          let gx = crds[k + crdIdx] + x + dx;
          let gy = crds[k + crdIdx + 1] + y + dy;
          let fx = lineX + gx * scale;
          let fy = lineY + gy * -scale;
          command.push(fx);
          command.push(fy);
          /*if (k === argCount - 2) {
            glyph.points.push({ x: fx, y: fy });
          }*/
        }
        crdIdx += argCount;
      }
      glyph.path.commands.push(command);
    }

    return { glyph, ax, ay };
  }

  _shapeToPaths(glyphs, line, { scale = 1, axs } = {}) {
    let x = 0, y = 0, paths = [];

    if (glyphs.length !== line.text.length) {
      throw Error('Invalid shape data');
    }

    // iterate over the glyphs, converting each to a glyph object
    // with a path property containing an array of commands
    for (let i = 0; i < glyphs.length; i++) {
      const { glyph, ax, ay } = this._singleShapeToPath(glyphs[i], {
        scale,
        x,
        y,
        lineX: line.x,
        lineY: line.y,
        axs
      });

      paths.push(glyph);
      x += ax; y += ay;
    }

    return paths;
  }

  _measureTextDefault(renderer, str) {
    let { textAlign, textBaseline } = renderer.states;
    let ctx = renderer.textDrawingContext();
    ctx.textAlign = 'left';
    ctx.textBaseline = 'alphabetic';
    let metrics = ctx.measureText(str);
    ctx.textAlign = textAlign;
    ctx.textBaseline = textBaseline;
    return metrics;
  }

  drawPaths(ctx, commands, opts) { // for debugging
    ctx.strokeStyle = opts?.stroke || ctx.strokeStyle;
    ctx.fillStyle = opts?.fill || ctx.fillStyle;
    ctx.beginPath();
    commands.forEach(([type, ...data]) => {
      if (type === 'M') {
        ctx.moveTo(...data);
      } else if (type === 'L') {
        ctx.lineTo(...data);
      } else if (type === 'C') {
        ctx.bezierCurveTo(...data);
      } else if (type === 'Q') {
        ctx.quadraticCurveTo(...data);
      } else if (type === 'Z') {
        ctx.closePath();
      }
    });
    if (opts?.fill) ctx.fill();
    if (opts?.stroke) ctx.stroke();
  }

  _pathsToCommands(paths, scale) {
    let commands = [];
    for (let i = 0; i < paths.length; i++) {
      let pathData = paths[i];
      let { x, y, path } = pathData;
      let { crds, cmds } = path;

      // iterate over the path, storing each non-control point
      for (let c = 0, j = 0; j < cmds.length; j++) {
        let cmd = cmds[j], obj = { type: cmd, data: [] };
        if (cmd === 'M' || cmd === 'L') {
          obj.data.push(x + crds[c] * scale, y + crds[c + 1] * -scale);
          c += 2;
        }
        else if (cmd === 'C') {
          for (let i = 0; i < 6; i += 2) {
            obj.data.push(
              x + crds[c + i] * scale, y + crds[c + i + 1] * -scale
            );
          }
          c += 6;
        }
        else if (cmd === 'Q') {
          for (let i = 0; i < 4; i += 2) {
            obj.data.push(
              x + crds[c + i] * scale, y + crds[c + i + 1] * -scale
            );
          }
          c += 4;
        }
        commands.push(obj);
      }
    }

    return commands;
  }
}

async function create(pInst, name, path, descriptors, rawFont) {

  let face = createFontFace(name, path, descriptors, rawFont);

  // load if we need to
  if (face.status !== 'loaded') await face.load();

  // add it to the document
  document.fonts.add(face);

  // ensure the font is ready to be rendered
  await document.fonts.ready;

  // return a new p5.Font
  return new Font(pInst, face, name, path, rawFont);
}


function sanitizeFontName(name) {
  if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) {
    name = "'" + String(name).replace(/'/g, "\\'") + "'";
  }
  return name;
}

function createFontFace(name, path, descriptors, rawFont) {

  name = sanitizeFontName(name);
  let fontArg = rawFont?._compressedData ?? rawFont?._data;
  if (!fontArg) {
    if (!validFontTypesRe.test(path)) {
      throw Error(invalidFontError);
    }
    if (!path.startsWith('url(')) {
      path = 'url(' + path + ')';
    }
    fontArg = path;
  }

  if ((rawFont?.fvar?.length ?? 0) > 0) {
    descriptors = descriptors || {};
    for (const [
      tag, minVal, defaultVal, maxVal, flags, name
    ] of rawFont.fvar[0]) {
      if (tag === 'wght') {
        descriptors.weight = `${minVal} ${maxVal}`;
      } else if (tag === 'wdth') {
        descriptors.stretch = `${minVal}% ${maxVal}%`;
      }
      // TODO add other descriptors
    }
  }

  // create/return the FontFace object
  let face = new FontFace(name, fontArg, descriptors);
  if (face.status === 'error') {
    throw Error('Failed to create FontFace for "' + name + '"');
  }
  return face;
}

function extractFontName(font, path) {
  let result, meta = font?.name;

  // use the metadata if we have it
  if (meta) {
    if (meta.fullName) {
      return meta.fullName;
    }
    if (meta.familyName) {
      result = meta.familyName;
    }
  }

  if (!result) {

    // if not, try to extract the name from the path
    let matches = extractFontNameRe.exec(path);
    if (matches && matches.length >= 3) {
      result = matches[1];
    }
    else {
      // give up and return the full path
      result = path;
    }
  }

  // replace spaces with underscores
  if (result.includes(' ')) {
    result = result.replace(/ /g, '_');
  }

  return result;
}
function pathToPoints(cmds, options, font) {

  const parseOpts = (options, defaults) => {
    if (typeof options !== 'object') {
      options = defaults;
    } else {
      for (const key in defaults) {
        if (typeof options[key] === 'undefined') {
          options[key] = defaults[key];
        }
      }
    }
    return options;
  };

  const at = (v, i) => {
    const s = v.length;
    return v[i < 0 ? i % s + s : i % s];
  };

  const simplify = (pts, angle) => {
    angle = angle || 0;
    let num = 0;
    for (let i = pts.length - 1; pts.length > 3 && i >= 0; --i) {
      if (collinear(at(pts, i - 1), at(pts, i), at(pts, i + 1), angle)) {
        pts.splice(i % pts.length, 1); // Remove middle point
        num++;
      }
    }
    return num;
  };

  const collinear = (a, b, c, thresholdAngle) => {
    if (!thresholdAngle) {
      return areaTriangle(a, b, c) === 0;
    }

    if (typeof collinear.tmpPoint1 === 'undefined') {
      collinear.tmpPoint1 = [];
      collinear.tmpPoint2 = [];
    }

    const ab = collinear.tmpPoint1,
      bc = collinear.tmpPoint2;
    ab.x = b.x - a.x;
    ab.y = b.y - a.y;
    bc.x = c.x - b.x;
    bc.y = c.y - b.y;

    const dot = ab.x * bc.x + ab.y * bc.y,
      magA = Math.sqrt(ab.x * ab.x + ab.y * ab.y),
      magB = Math.sqrt(bc.x * bc.x + bc.y * bc.y),
      angle = Math.acos(dot / (magA * magB));

    return angle < thresholdAngle;
  };

  const areaTriangle = (a, b, c) => {
    return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
  };

  const path = createFromCommands(arrayCommandsToObjects(cmds));
  let opts = parseOpts(options, {
    sampleFactor: 0.1,
    simplifyThreshold: 0
  });

  const totalPoints = Math.max(
    1,
    Math.ceil(path.getTotalLength() * opts.sampleFactor)
  );
  let points = [];

  const mode = font._pInst.angleMode();
  const DEGREES = font._pInst.DEGREES;
  for (let i = 0; i < totalPoints; i++) {
    const length = path.getTotalLength() * (
      totalPoints === 1
        ? 0
        : (i / (totalPoints - 1))
    );
    points.push({
      ...path.getPointAtLength(length),
      get angle() {
        const angle = path.getAngleAtLength(length);
        if (mode === DEGREES) {
          return angle * 180 / Math.PI;
        } else {
          return angle;
        }
      },
      // For backwards compatibility
      get alpha() {
        return this.angle;
      }
    });
  }

  if (opts.simplifyThreshold) {
    simplify(points, opts.simplifyThreshold);
  }

  return points;
}

function unquote(name) {
  // Unquote name from CSS
  if ((name.startsWith('"') || name.startsWith("'")) && name.at(0) === name.at(-1)) {
    return name.slice(1, -1).replace(/\/(['"])/g, '$1');
  }
  return name;
}

function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {

  // parse the path
  let path = args.shift();
  if (typeof path !== 'string' || path.length === 0) {
    p5._friendlyError(invalidFontError, 'p5.loadFont'); // ?
  }

  // parse the name
  let name;
  if (typeof args[0] === 'string') {
    name = args.shift();
  }

  // get the callbacks/descriptors if any
  let success, error, options;
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (typeof arg === 'function') {
      if (!success) {
        success = arg;
      } else {
        error = arg;
      }
    }
    else if (typeof arg === 'object') {
      options = arg;
    }
  }

  return { path, name, success, error, options };
}

function font(p5, fn) {

  /**
   * A class to describe fonts. Create through <a href="#/p5/loadFont">`loadFont()`</a>.
   *
   * @class p5.Font
   */
  p5.Font = Font;

  /**
   * @private
   */
  fn.parseFontData = async function(pathOrData) {
    // load the raw font bytes
    let result = pathOrData instanceof Uint8Array
      ? pathOrData
      : await fn.loadBytes(pathOrData);
    //console.log('result:', result);

    if (!result) {
      throw Error('Failed to load font data');
    }

    // parse the font data
    let fonts = Typr.parse(result);

    // TODO: generate descriptors from font in the future

    if (fonts.length === 0 || fonts[0].cmap === undefined) {
      throw Error('parsing font data');
    }

    return fonts[0];
  };

  /**
   * Loads a font and creates a <a href="#/p5.Font">p5.Font</a> object.
   * `loadFont()` can load fonts in either .otf or .ttf format. Loaded fonts can
   * be used to style text on the canvas and in HTML elements.
   *
   * The first parameter, `path`, is the path to a font file.
   * Paths to local files should be relative. For example,
   * `'assets/inconsolata.otf'`. The Inconsolata font used in the following
   * examples can be downloaded for free
   * <a href="https://www.fontsquirrel.com/fonts/inconsolata" target="_blank">here</a>.
   * Paths to remote files should be URLs. For example,
   * `'https://example.com/inconsolata.otf'`. URLs may be blocked due to browser
   * security.
   *
   * In 2D mode, `path` can take on a few other forms. It could be a path to a CSS file,
   * such as one from <a href="https://fonts.google.com/">Google Fonts.</a> It could also
   * be a string with a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face">CSS `@font-face` declaration.</a>
   *
   * The second parameter, `successCallback`, is optional. If a function is
   * passed, it will be called once the font has loaded. The callback function
   * may use the new <a href="#/p5.Font">p5.Font</a> object if needed.
   *
   * The third parameter, `failureCallback`, is also optional. If a function is
   * passed, it will be called if the font fails to load. The callback function
   * may use the error
   * <a href="https://developer.mozilla.org/en-US/docs/Web/API/Event" target="_blank">Event</a>
   * object if needed.
   *
   * Fonts can take time to load. `await` the result of `loadFont()` in
   * <a href="#/p5/setup">setup()</a> before using the result.
   *
   * @method loadFont
   * @for p5
   * @param  {String}        path       path of the font or CSS file to be loaded, or a CSS `@font-face` string.
   * @param  {String}        [name]            An alias that can be used for this font in `textFont()`. Defaults to the name in the font's metadata.
   * @param  {Object}        [options]         An optional object with extra CSS font face descriptors, or p5.js font settings.
   * @param  {String|Array<String>} [options.sets] (Experimental) An optional string of list of strings with Unicode character set names that should be included. When a CSS file is used as the font, it may contain multiple font files. The font best matching the requested character sets will be picked.
   * @param  {Function}      [successCallback] function called with the
   *                                           <a href="#/p5.Font">p5.Font</a> object after it
   *                                           loads.
   * @param  {Function}      [failureCallback] function called with the error
   *                                           <a href="https://developer.mozilla.org/en-US/docs/Web/API/Event" target="_blank">Event</a>
   *                                           object if the font fails to load.
   * @return {Promise<p5.Font>}                         <a href="#/p5.Font">p5.Font</a> object.
   * @example
   * let font;
   *
   * async function setup() {
   *   createCanvas(100, 100);
   *   font = await loadFont('assets/inconsolata.otf');
   *   fill('deeppink');
   *   textFont(font);
   *   textSize(36);
   *   text('p5*js', 10, 50);
   *
   *   describe('The text "p5*js" written in pink on a white background.');
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *   loadFont('assets/inconsolata.otf', font => {
   *     fill('deeppink');
   *     textFont(font);
   *     textSize(36);
   *     text('p5*js', 10, 50);
   *
   *     describe('The text "p5*js" written in pink on a white background.');
   *   });
   * }
   *
   * @example
   * function setup() {
   *   createCanvas(100, 100);
   *   loadFont('assets/inconsolata.otf', success, failure);
   * }
   *
   * function success(font) {
   *   fill('deeppink');
   *   textFont(font);
   *   textSize(36);
   *   text('p5*js', 10, 50);
   *
   *   describe('The text "p5*js" written in pink on a white background.');
   * }
   *
   * function failure(event) {
   *   console.error('Oops!', event);
   * }
   *
   * @example
   * async function setup() {
   *   createCanvas(100, 100);
   *   await loadFont('assets/inconsolata.otf');
   *   let p = createP('p5*js');
   *   p.style('color', 'deeppink');
   *   p.style('font-family', 'Inconsolata');
   *   p.style('font-size', '36px');
   *   p.position(10, 50);
   *
   *   describe('The text "p5*js" written in pink on a white background.');
   * }
   *
   * @example
   * // META:norender
   * // Some other forms of loading fonts:
   * loadFont("https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap");
   *
   * loadFont('@font-face { font-family: "Bricolage Grotesque", serif; font-optical-sizing: auto; font-weight: 400; font-style: normal; font-variation-settings: "wdth" 100; }');
   */
  /**
   * @method loadFont
   * @for p5
   * @param  {String}        path              path of the font to be loaded.
   * @param  {Function}      [successCallback] function called with the
   *                                           <a href="#/p5.Font">p5.Font</a> object after it
   *                                           loads.
   * @param  {Function}      [failureCallback] function called with the error
   *                                           <a href="https://developer.mozilla.org/en-US/docs/Web/API/Event" target="_blank">Event</a>
   *                                           object if the font fails to load.
   * @returns {Promise<p5.Font>} The font.
   */
  fn.loadFont = async function (...args) {
    /*path, name, onSuccess, onError, descriptors*/
    let {
      path,
      name,
      success,
      error,
      options: { sets, ...descriptors } = {}
    } = parseCreateArgs(...args);

    let isCSS = path.includes('@font-face');

    if (!isCSS) {
      let info;
      try {
        info = await fetch(path, { method: 'HEAD' });
      } catch (e) {
        // Sometimes files fail when requested with HEAD. Fallback to a
        // regular GET. It loads more data, but at least then it's cached
        // for the likely case when we have to fetch the whole thing.
        info = await fetch(path);
      }
      const isCSSFile = info.headers.get('content-type')?.startsWith('text/css');
      if (isCSSFile) {
        isCSS = true;
        path = await fetch(path).then(res => res.text());
      }
    }

    if (isCSS) {
      const stylesheet = new CSSStyleSheet();
      await stylesheet.replace(path);
      const possibleFonts = [];
      for (const rule of stylesheet.cssRules) {
        if (rule instanceof CSSFontFaceRule) {
          const style = rule.style;
          let name = unquote(style.getPropertyValue('font-family'));
          const src = style.getPropertyValue('src');
          const fontDescriptors = { ...(descriptors || {}) };
          for (const key of style) {
            if (key === 'font-family' || key === 'src') continue;
            const camelCaseKey = key
              .replace(/^font-/, '')
              .split('-')
              .map((v, i) => i === 0 ? v : `${v[0].toUpperCase()}${v.slice(1)}`)
              .join('');
            fontDescriptors[camelCaseKey] = style.getPropertyValue(key);
          }
          possibleFonts.push({
            name,
            src,
            fontDescriptors,
            loadWithData: async () => {
              let fontData;
              try {
                const urlMatch = /url\(([^\)]+)\)/.exec(src);
                if (urlMatch) {
                  let url = urlMatch[1];
                  if (/^['"]/.exec(url) && url.at(0) === url.at(-1)) {
                    url = url.slice(1, -1);
                  }
                  fontData = await fn.parseFontData(url);
                }
              } catch (_e) {}
              return create(this, name, src, fontDescriptors, fontData);
            },
            loadWithoutData: () => create(this, name, src, fontDescriptors)
          });
        }
      }

      // TODO: handle multiple font faces?
      sets = sets || ['latin']; // Default to latin for now if omitted
      const requestedGroups = (sets instanceof Array ? sets : [sets])
        .map(s => s.toLowerCase());
      // Grab thr named groups with names that include the requested keywords
      const requestedCategories = unicodeRanges
        .filter(r => requestedGroups.some(
          g => r.category.includes(g) &&
            // Only include extended character sets if specifically requested
            r.category.includes('ext') === g.includes('ext')
        ));
      const requestedRanges = new Set(
        UnicodeRange.parse(
          requestedCategories.map(c => `U+${c.hexrange[0]}-${c.hexrange[1]}`)
        )
      );
      let closestRangeOverlap = 0;
      let closestDescriptorOverlap = 0;
      let closestMatch = undefined;
      for (const font of possibleFonts) {
        if (!font.fontDescriptors.unicodeRange) continue;
        const fontRange = new Set(
          UnicodeRange.parse(
            font.fontDescriptors.unicodeRange.split(/,\s*/g)
          )
        );
        const rangeOverlap = [...fontRange.values()]
          .filter(v => requestedRanges.has(v))
          .length;

        const targetDescriptors = {
          // Default to normal style at regular weight
          style: 'normal',
          weight: 400,
          // Override from anything else passed in
          ...descriptors
        };
        const descriptorOverlap = Object.keys(font.fontDescriptors)
          .filter(k => font.fontDescriptors[k] === targetDescriptors[k])
          .length;

        if (
          descriptorOverlap > closestDescriptorOverlap ||
          (
            descriptorOverlap === closestDescriptorOverlap &&
            rangeOverlap >= closestRangeOverlap
          )
        ) {
          closestDescriptorOverlap = descriptorOverlap;
          closestRangeOverlap = rangeOverlap;
          closestMatch = font;
        }
      }
      const picked = (closestMatch || possibleFonts.at(-1));
      for (const font of possibleFonts) {
        if (font !== picked) {
          // Load without parsing data with Typr so that it still can be accessed
          // via regular CSS by name
          font.loadWithoutData();
        }
      }
      return picked?.loadWithData();
    }

    let pfont;
    try {
      const fontData = await fn.parseFontData(path);

      // make sure we have a valid name
      name = name || extractFontName(fontData, path);

      // create a FontFace object and pass it to the p5.Font constructor
      pfont = await create(this, name, path, descriptors, fontData);

    } catch (err) {
      // failed to parse the font, load it as a simple FontFace
      let ident = name || path
        .substring(path.lastIndexOf('/') + 1)
        .replace(/\.[^/.]+$/, '');

      console.warn(`WARN: No glyph data for '${ident}', retrying as FontFace`);

      try {
        // create a FontFace object and pass it to p5.Font
        pfont = await create(this, ident, path, descriptors);
      }
      catch (err) {
        if (error) return error(err);
        throw err;
      }
    }
    const cb = () => {
      if (success) return success(pfont);
      return pfont;
    };
    return this._internal ? this._internal(cb) : cb();
  };
}
// Convert arrays to named objects
const arrayCommandsToObjects = commands => commands.map(command => {
  const type = command[0];
  switch (type) {
    case 'Z': {
      return { type };
    }
    case 'M':
    case 'L': {
      const [, x, y] = command;
      return { type, x, y };
    }
    case 'Q': {
      const [, x1, y1, x, y] = command;
      return { type, x1, y1, x, y };
    }
    case 'C': {
      const [, x1, y1, x2, y2, x, y] = command;
      return { type, x1, y1, x2, y2, x, y };
    }
    default: {
      throw new Error(`Unexpected path command: ${type}`);
    }
  }
});

if (typeof p5 !== 'undefined') {
  font(p5, p5.prototype);
}

export { Font, sanitizeFontName as _sanitizeFontName, arrayCommandsToObjects, font as default };